Commit 7f1b934e authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into 'prettify-all-the-karma-specs'

# Conflicts:
#   spec/javascripts/clusters/components/application_row_spec.js
parents 7a3e6053 1d7453d3
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue'; import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue';
import { import {
APPLICATION_STATUS, APPLICATION_STATUS,
REQUEST_LOADING, REQUEST_LOADING,
REQUEST_SUCCESS, REQUEST_SUCCESS,
REQUEST_FAILURE, REQUEST_FAILURE,
} from '../constants'; } from '../constants';
export default { export default {
components: { components: {
loadingButton, loadingButton,
identicon, identicon,
},
props: {
id: {
type: String,
required: true,
}, },
props: { title: {
id: { type: String,
type: String, required: true,
required: true,
},
title: {
type: String,
required: true,
},
titleLink: {
type: String,
required: false,
},
manageLink: {
type: String,
required: false,
},
logoUrl: {
type: String,
required: false,
default: null,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
status: {
type: String,
required: false,
},
statusReason: {
type: String,
required: false,
},
requestStatus: {
type: String,
required: false,
},
requestReason: {
type: String,
required: false,
},
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
}, },
computed: { titleLink: {
isUnknownStatus() { type: String,
return !this.isKnownStatus && this.status !== null; required: false,
}, },
isKnownStatus() { manageLink: {
return Object.values(APPLICATION_STATUS).includes(this.status); type: String,
}, required: false,
isInstalled() { },
return ( logoUrl: {
this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED type: String,
); required: false,
}, default: null,
hasLogo() { },
return !!this.logoUrl; disabled: {
}, type: Boolean,
identiconId() { required: false,
// generate a deterministic integer id for the identicon background default: false,
return this.id.charCodeAt(0); },
}, status: {
rowJsClass() { type: String,
return `js-cluster-application-row-${this.id}`; required: false,
}, },
installButtonLoading() { statusReason: {
return !this.status || type: String,
this.status === APPLICATION_STATUS.SCHEDULED || required: false,
this.status === APPLICATION_STATUS.INSTALLING || },
this.requestStatus === REQUEST_LOADING; requestStatus: {
}, type: String,
installButtonDisabled() { required: false,
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but },
// we already made a request to install and are just waiting for the real-time requestReason: {
// to sync up. type: String,
return ((this.status !== APPLICATION_STATUS.INSTALLABLE required: false,
&& this.status !== APPLICATION_STATUS.ERROR) || },
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
isUnknownStatus() {
return !this.isKnownStatus && this.status !== null;
},
isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
isInstalled() {
return (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
this.status === APPLICATION_STATUS.UPDATING
);
},
hasLogo() {
return !!this.logoUrl;
},
identiconId() {
// generate a deterministic integer id for the identicon background
return this.id.charCodeAt(0);
},
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
installButtonLoading() {
return (
!this.status ||
this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING ||
this.requestStatus === REQUEST_LOADING
);
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return (
((this.status !== APPLICATION_STATUS.INSTALLABLE &&
this.status !== APPLICATION_STATUS.ERROR) ||
this.requestStatus === REQUEST_LOADING || this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS) && this.isKnownStatus; this.requestStatus === REQUEST_SUCCESS) &&
}, this.isKnownStatus
installButtonLabel() { );
let label; },
if ( installButtonLabel() {
this.status === APPLICATION_STATUS.NOT_INSTALLABLE || let label;
this.status === APPLICATION_STATUS.INSTALLABLE || if (
this.status === APPLICATION_STATUS.ERROR || this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.isUnknownStatus this.status === APPLICATION_STATUS.INSTALLABLE ||
) { this.status === APPLICATION_STATUS.ERROR ||
label = s__('ClusterIntegration|Install'); this.isUnknownStatus
} else if (this.status === APPLICATION_STATUS.SCHEDULED || ) {
this.status === APPLICATION_STATUS.INSTALLING) { label = s__('ClusterIntegration|Install');
label = s__('ClusterIntegration|Installing'); } else if (
} else if (this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.UPDATED) { this.status === APPLICATION_STATUS.INSTALLING
label = s__('ClusterIntegration|Installed'); ) {
} label = s__('ClusterIntegration|Installing');
} else if (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
this.status === APPLICATION_STATUS.UPDATING
) {
label = s__('ClusterIntegration|Installed');
}
return label; return label;
}, },
showManageButton() { showManageButton() {
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED; return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
}, },
manageButtonLabel() { manageButtonLabel() {
return s__('ClusterIntegration|Manage'); return s__('ClusterIntegration|Manage');
}, },
hasError() { hasError() {
return this.status === APPLICATION_STATUS.ERROR || return this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE;
this.requestStatus === REQUEST_FAILURE; },
}, generalErrorDescription() {
generalErrorDescription() { return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
return sprintf( title: this.title,
s__('ClusterIntegration|Something went wrong while installing %{title}'), { });
title: this.title,
},
);
},
}, },
methods: { },
installClicked() { methods: {
eventHub.$emit('installApplication', { installClicked() {
id: this.id, eventHub.$emit('installApplication', {
params: this.installApplicationRequestParams, id: this.id,
}); params: this.installApplicationRequestParams,
}, });
}, },
}; },
};
</script> </script>
<template> <template>
......
...@@ -6,6 +6,7 @@ export const APPLICATION_STATUS = { ...@@ -6,6 +6,7 @@ export const APPLICATION_STATUS = {
INSTALLING: 'installing', INSTALLING: 'installing',
INSTALLED: 'installed', INSTALLED: 'installed',
UPDATED: 'updated', UPDATED: 'updated',
UPDATING: 'updating',
ERROR: 'errored', ERROR: 'errored',
}; };
......
...@@ -340,6 +340,8 @@ ...@@ -340,6 +340,8 @@
} }
.note-form-actions { .note-form-actions {
color: $gl-text-color;
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
.btn { .btn {
float: none; float: none;
......
...@@ -8,7 +8,7 @@ module SendsBlob ...@@ -8,7 +8,7 @@ module SendsBlob
include SendFileUpload include SendFileUpload
end end
def send_blob(blob, params = {}) def send_blob(repository, blob, params = {})
if blob if blob
headers['X-Content-Type-Options'] = 'nosniff' headers['X-Content-Type-Options'] = 'nosniff'
......
...@@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController ...@@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController
def show def show
@blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git) @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
send_blob(@blob) send_blob(@repository, @blob)
end end
def destroy def destroy
......
...@@ -12,6 +12,6 @@ class Projects::RawController < Projects::ApplicationController ...@@ -12,6 +12,6 @@ class Projects::RawController < Projects::ApplicationController
def show def show
@blob = @repository.blob_at(@commit.id, @path) @blob = @repository.blob_at(@commit.id, @path)
send_blob(@blob, inline: (params[:inline] != 'false')) send_blob(@repository, @blob, inline: (params[:inline] != 'false'))
end end
end end
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Projects::WikisController < Projects::ApplicationController class Projects::WikisController < Projects::ApplicationController
include PreviewMarkdown include PreviewMarkdown
include SendsBlob
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
before_action :authorize_read_wiki! before_action :authorize_read_wiki!
...@@ -26,16 +27,8 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -26,16 +27,8 @@ class Projects::WikisController < Projects::ApplicationController
set_encoding_error unless valid_encoding? set_encoding_error unless valid_encoding?
render 'show' render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id]) elsif file_blob
response.headers['Content-Security-Policy'] = "default-src 'none'" send_blob(@project_wiki.repository, file_blob)
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
send_data(
file.raw_data,
type: file.mime_type,
disposition: 'inline',
filename: file.name
)
elsif can?(current_user, :create_wiki, @project) && view_param == 'create' elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
@page = build_page(title: params[:id]) @page = build_page(title: params[:id])
...@@ -164,4 +157,14 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -164,4 +157,14 @@ class Projects::WikisController < Projects::ApplicationController
def set_encoding_error def set_encoding_error
flash.now[:notice] = "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." flash.now[:notice] = "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
end end
def file_blob
strong_memoize(:file_blob) do
commit = @project_wiki.repository.commit(@project_wiki.default_branch)
next unless commit
@project_wiki.repository.blob_at(commit.id, params[:id])
end
end
end end
...@@ -150,7 +150,9 @@ module BlobHelper ...@@ -150,7 +150,9 @@ module BlobHelper
# example of Javascript) we tell the browser of the victim not to # example of Javascript) we tell the browser of the victim not to
# execute untrusted data. # execute untrusted data.
def safe_content_type(blob) def safe_content_type(blob)
if blob.text? if blob.extension == 'svg'
blob.mime_type
elsif blob.text?
'text/plain; charset=utf-8' 'text/plain; charset=utf-8'
elsif blob.image? elsif blob.image?
blob.content_type blob.content_type
...@@ -159,6 +161,12 @@ module BlobHelper ...@@ -159,6 +161,12 @@ module BlobHelper
end end
end end
def content_disposition(blob, inline)
return 'attachment' if blob.extension == 'svg'
inline ? 'inline' : 'attachment'
end
def ref_project def ref_project
@ref_project ||= @target_project || @project @ref_project ||= @target_project || @project
end end
......
...@@ -6,7 +6,7 @@ module WorkhorseHelper ...@@ -6,7 +6,7 @@ module WorkhorseHelper
# Send a Git blob through Workhorse # Send a Git blob through Workhorse
def send_git_blob(repository, blob, inline: true) def send_git_blob(repository, blob, inline: true)
headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob)) headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob))
headers['Content-Disposition'] = inline ? 'inline' : 'attachment' headers['Content-Disposition'] = content_disposition(blob, inline)
headers['Content-Type'] = safe_content_type(blob) headers['Content-Type'] = safe_content_type(blob)
render plain: "" render plain: ""
end end
......
...@@ -56,7 +56,6 @@ class PrometheusService < MonitoringService ...@@ -56,7 +56,6 @@ class PrometheusService < MonitoringService
name: 'api_url', name: 'api_url',
title: 'API URL', title: 'API URL',
placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'), placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'),
help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'),
required: true required: true
} }
] ]
......
...@@ -487,7 +487,20 @@ class Repository ...@@ -487,7 +487,20 @@ class Repository
end end
def blob_at(sha, path) def blob_at(sha, path)
Blob.decorate(raw_repository.blob_at(sha, path), project) blob = Blob.decorate(raw_repository.blob_at(sha, path), project)
# Don't attempt to return a special result if there is no blob at all
return unless blob
# Don't attempt to return a special result unless we're looking at HEAD
return blob unless head_commit&.sha == sha
case path
when head_tree&.readme_path
ReadmeBlob.new(blob, self)
else
blob
end
rescue Gitlab::Git::Repository::NoRepository rescue Gitlab::Git::Repository::NoRepository
nil nil
end end
...@@ -569,9 +582,7 @@ class Repository ...@@ -569,9 +582,7 @@ class Repository
cache_method :merge_request_template_names, fallback: [] cache_method :merge_request_template_names, fallback: []
def readme def readme
if readme = tree(:head)&.readme head_tree&.readme
ReadmeBlob.new(readme, self)
end
end end
def rendered_readme def rendered_readme
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Tree class Tree
include Gitlab::MarkupHelper include Gitlab::MarkupHelper
include Gitlab::Utils::StrongMemoize
attr_accessor :repository, :sha, :path, :entries attr_accessor :repository, :sha, :path, :entries
...@@ -16,32 +17,36 @@ class Tree ...@@ -16,32 +17,36 @@ class Tree
@entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive) @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive)
end end
def readme def readme_path
return @readme if defined?(@readme) strong_memoize(:readme_path) do
available_readmes = blobs.select do |blob|
available_readmes = blobs.select do |blob| Gitlab::FileDetector.type_of(blob.name) == :readme
Gitlab::FileDetector.type_of(blob.name) == :readme end
end
previewable_readmes = available_readmes.select do |blob|
previewable_readmes = available_readmes.select do |blob| previewable?(blob.name)
previewable?(blob.name) end
end
plain_readmes = available_readmes.select do |blob|
plain_readmes = available_readmes.select do |blob| plain?(blob.name)
plain?(blob.name) end
# Prioritize previewable over plain readmes
entry = previewable_readmes.first || plain_readmes.first
next nil unless entry
if path == '/'
entry.name
else
File.join(path, entry.name)
end
end end
end
# Prioritize previewable over plain readmes def readme
readme_tree = previewable_readmes.first || plain_readmes.first strong_memoize(:readme) do
repository.blob_at(sha, readme_path) if readme_path
# Return if we can't preview any of them
if readme_tree.nil?
return @readme = nil
end end
readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
@readme = repository.blob_at(sha, readme_path)
end end
def trees def trees
......
...@@ -160,7 +160,9 @@ class WikiPage ...@@ -160,7 +160,9 @@ class WikiPage
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
# is an old version of the page. # is an old version of the page.
def historical? def historical?
@page.historical? && last_version.sha != version.sha return false unless last_commit_sha && version
@page.historical? && last_commit_sha != version.sha
end end
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
......
...@@ -2,4 +2,5 @@ ...@@ -2,4 +2,5 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page) = link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format}) %small (#{wiki_page.format})
.float-right .float-right
%small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe - if wiki_page.last_version
%small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe
...@@ -11,8 +11,9 @@ ...@@ -11,8 +11,9 @@
.nav-text .nav-text
%h2.wiki-page-title= @page.title.capitalize %h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by %span.wiki-last-edit-by
= (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe - if @page.last_version
#{time_ago_with_tooltip(@page.last_version.authored_date)} = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
#{time_ago_with_tooltip(@page.last_version.authored_date)}
.nav-controls .nav-controls
= render 'main_links' = render 'main_links'
......
---
title: Use cached readme contents when available
merge_request: 22325
author:
type: performance
---
title: Fix a bug displaying certain wiki pages
merge_request: 22377
author:
type: fixed
---
title: Fix bug with wiki attachments content disposition
merge_request: 22220
author:
type: fixed
---
title: Remove prometheus configuration help text
merge_request: 22413
author: George Tsiolis
type: other
...@@ -148,6 +148,9 @@ This label documents the planned timeline & urgency which is used to measure aga ...@@ -148,6 +148,9 @@ This label documents the planned timeline & urgency which is used to measure aga
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter or 90 days) | | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter or 90 days) |
| ~P4 | Low Priority | Anything outside the next 3 releases (more than one quarter or 120 days) | | ~P4 | Low Priority | Anything outside the next 3 releases (more than one quarter or 120 days) |
If an issue seems to fall between two priority labels, assign it to the higher-
priority label.
## Severity labels ## Severity labels
Severity labels help us clearly communicate the impact of a ~bug on users. Severity labels help us clearly communicate the impact of a ~bug on users.
...@@ -159,6 +162,10 @@ Severity labels help us clearly communicate the impact of a ~bug on users. ...@@ -159,6 +162,10 @@ Severity labels help us clearly communicate the impact of a ~bug on users.
| ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. | | ~S3 | Major Severity | Broken Feature, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue. |
| ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. | | ~S4 | Low Severity | Functionality inconvenience or cosmetic issue | Label colors are incorrect / not being displayed. |
If an issue seems to fall between two severity labels, even taking the
[severity impact guidance](#severity-impact-guidance) into account, assign
it to the higher-severity label.
### Severity impact guidance ### Severity impact guidance
Severity levels can be applied further depending on the facet of the impact; e.g. Affected customers, GitLab.com availability, performance and etc. The below is a guideline. Severity levels can be applied further depending on the facet of the impact; e.g. Affected customers, GitLab.com availability, performance and etc. The below is a guideline.
......
...@@ -72,7 +72,7 @@ module Gitlab ...@@ -72,7 +72,7 @@ module Gitlab
# and `safe_max_bytes` # and `safe_max_bytes`
# #
# :expanded :: # :expanded ::
# If true, patch raw data will not be included in the diff after # If false, patch raw data will not be included in the diff after
# `max_files`, `max_lines` or any of the limits in `limits` are # `max_files`, `max_lines` or any of the limits in `limits` are
# exceeded # exceeded
def filter_diff_options(options, default_options = {}) def filter_diff_options(options, default_options = {})
......
...@@ -4908,9 +4908,6 @@ msgstr "" ...@@ -4908,9 +4908,6 @@ msgstr ""
msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments" msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
msgstr "" msgstr ""
msgid "PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server."
msgstr ""
msgid "PrometheusService|Common metrics" msgid "PrometheusService|Common metrics"
msgstr "" msgstr ""
......
...@@ -36,6 +36,7 @@ module QA ...@@ -36,6 +36,7 @@ module QA
# GitLab QA fabrication mechanisms # GitLab QA fabrication mechanisms
# #
module Factory module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base' autoload :Base, 'qa/factory/base'
autoload :Dependency, 'qa/factory/dependency' autoload :Dependency, 'qa/factory/dependency'
autoload :Product, 'qa/factory/product' autoload :Product, 'qa/factory/product'
......
# Factory objects in GitLab QA
In GitLab QA we are using factories to create resources.
Factories implementation are primarily done using Browser UI steps, but can also
be done via the API.
## Why do we need that?
We need factory objects because we need to reduce duplication when creating
resources for our QA tests.
## How to properly implement a factory object?
All factories should inherit from [`Factory::Base`](./base.rb).
There is only one mandatory method to implement to define a factory. This is the
`#fabricate!` method, which is used to build a resource via the browser UI.
Note that you should only use [Page objects](../page/README.md) to interact with
a Web page in this method.
Here is an imaginary example:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
def initialize(name)
@name = name
end
def fabricate!
Page::Dashboard::Index.perform do |dashboard_index|
dashboard_index.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end
end
end
end
end
```
### Define API implementation
A factory may also implement the three following methods to be able to create a
resource via the public GitLab API:
- `#api_get_path`: The `GET` path to fetch an existing resource.
- `#api_post_path`: The `POST` path to create a new resource.
- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource.
Let's take the `Shirt` factory example, and add these three API methods:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
def initialize(name)
@name = name
end
def fabricate!
Page::Dashboard::Index.perform do |dashboard_index|
dashboard_index.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end
def api_get_path
"/shirt/#{name}"
end
def api_post_path
"/shirts"
end
def api_post_body
{
name: name
}
end
end
end
end
end
```
The [`Project` factory](./resource/project.rb) is a good real example of Browser
UI and API implementations.
### Define dependencies
A resource may need an other resource to exist first. For instance, a project
needs a group to be created in.
To define a dependency, you can use the `dependency` DSL method.
The first argument is a factory class, then you should pass `as: <name>` to give
a name to the dependency.
That will allow access to the dependency from your resource object's methods.
You would usually use it in `#fabricate!`, `#api_get_path`, `#api_post_path`,
`#api_post_body`.
Let's take the `Shirt` factory, and add a `project` dependency to it:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-create-a-shirt'
end
def initialize(name)
@name = name
end
def fabricate!
project.visit!
Page::Project::Show.perform do |project_show|
project_show.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end
def api_get_path
"/project/#{project.path}/shirt/#{name}"
end
def api_post_path
"/project/#{project.path}/shirts"
end
def api_post_body
{
name: name
}
end
end
end
end
end
```
**Note that dependencies are always built via the API fabrication method if
supported by their factories.**
### Define attributes on the created resource
Once created, you may want to populate a resource with attributes that can be
found in the Web page, or in the API response.
For instance, once you create a project, you may want to store its repository
SSH URL as an attribute.
To define an attribute, you can use the `product` DSL method.
The first argument is the attribute name, then you should define a name for the
dependency to be accessible from your resource object's methods.
Let's take the `Shirt` factory, and define a `:brand` attribute:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-create-a-shirt'
end
# Attribute populated from the Browser UI (using the block)
product :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
def initialize(name)
@name = name
end
def fabricate!
project.visit!
Page::Project::Show.perform do |project_show|
project_show.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end
def api_get_path
"/project/#{project.path}/shirt/#{name}"
end
def api_post_path
"/project/#{project.path}/shirts"
end
def api_post_body
{
name: name
}
end
end
end
end
end
```
#### Inherit a factory's attribute
Sometimes, you want a resource to inherit its factory attributes. For instance,
it could be useful to pass the `size` attribute from the `Shirt` factory to the
created resource.
You can do that by defining `product :attribute_name` without a block.
Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-create-a-shirt'
end
# Attribute inherited from the Shirt factory if present,
# or from the Browser UI otherwise (using the block)
product :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
# Attribute inherited from the Shirt factory if present,
# or a QA::Factory::Product::NoValueError is raised otherwise
product :name
product :size
def initialize(name)
@name = name
end
def fabricate!
project.visit!
Page::Project::Show.perform do |project_show|
project_show.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end
def api_get_path
"/project/#{project.path}/shirt/#{name}"
end
def api_post_path
"/project/#{project.path}/shirts"
end
def api_post_body
{
name: name
}
end
end
end
end
end
```
#### Define an attribute based on an API response
Sometimes, you want to define a resource attribute based on the API response
from its `GET` or `POST` request. For instance, if the creation of a shirt via
the API returns
```ruby
{
brand: 'a-brand-new-brand',
size: 'extra-small',
style: 't-shirt',
materials: [[:cotton, 80], [:polyamide, 20]]
}
```
you may want to store `style` as-is in the resource, and fetch the first value
of the first `materials` item in a `main_fabric` attribute.
For both attributes, you will need to define an inherited attribute, as shown
in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you
will need to implement the
`#transform_api_resource` method to first populate the `:main_fabric` key in the
API response so that it can be used later to automatically populate the
attribute on your resource.
If an attribute can only be retrieved from the API response, you should define
a block to give it a default value, otherwise you could get a
`QA::Factory::Product::NoValueError` when creating your resource via the
Browser UI.
Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric`
attributes:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-create-a-shirt'
end
# Attribute fetched from the API response if present if present,
# or from the Shirt factory if present,
# or from the Browser UI otherwise (using the block)
product :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
# Attribute fetched from the API response if present if present,
# or from the Shirt factory if present,
# or a QA::Factory::Product::NoValueError is raised otherwise
product :name
product :size
product :style do
'unknown'
end
product :main_fabric do
'unknown'
end
def initialize(name)
@name = name
end
def fabricate!
project.visit!
Page::Project::Show.perform do |project_show|
project_show.go_to_new_shirt
end
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
end
end
def api_get_path
"/project/#{project.path}/shirt/#{name}"
end
def api_post_path
"/project/#{project.path}/shirts"
end
def api_post_body
{
name: name
}
end
private
def transform_api_resource(api_response)
api_response[:main_fabric] = api_response[:materials][0][0]
api_response
end
end
end
end
end
```
**Notes on attributes precedence:**
- attributes from the API response take precedence over attributes from the
factory (i.e inherited)
- attributes from the factory (i.e inherited) take precedence over attributes
from the Browser UI
- attributes without a value will raise a `QA::Factory::Product::NoValueError` error
## Creating resources in your tests
To create a resource in your tests, you can call the `.fabricate!` method on the
factory class.
Note that if the factory supports API fabrication, this will use this
fabrication by default.
Here is an example that will use the API fabrication method under the hood since
it's supported by the `Shirt` factory:
```ruby
my_shirt = Factory::Resource::Shirt.fabricate!('my-shirt') do |shirt|
shirt.size = 'small'
end
expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
expect(page).to have_text(my_shirt.size) # => "extra-small" from the API response
expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the (transformed) API response
```
If you explicitely want to use the Browser UI fabrication method, you can call
the `.fabricate_via_browser_ui!` method instead:
```ruby
my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui!('my-shirt') do |shirt|
shirt.size = 'small'
end
expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute
expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block
expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block
```
You can also explicitely use the API fabrication method, by calling the
`.fabricate_via_api!` method:
```ruby
my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt|
shirt.size = 'small'
end
```
In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`.
## Where to ask for help?
If you need more information, ask for help on `#quality` channel on Slack
(internal, GitLab Team only).
If you are not a Team Member, and you still need help to contribute, please
open an issue in GitLab CE issue tracker with the `~QA` label.
# frozen_string_literal: true
require 'airborne'
require 'active_support/core_ext/object/deep_dup'
require 'capybara/dsl'
module QA
module Factory
module ApiFabricator
include Airborne
include Capybara::DSL
HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
ResourceNotFoundError = Class.new(RuntimeError)
ResourceFabricationFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
attr_reader :api_resource, :api_response
def api_support?
respond_to?(:api_get_path) &&
respond_to?(:api_post_path) &&
respond_to?(:api_post_body)
end
def fabricate_via_api!
unless api_support?
raise NotImplementedError, "Factory #{self.class.name} does not support fabrication via the API!"
end
resource_web_url(api_post)
end
def eager_load_api_client!
api_client.tap do |client|
# Eager-load the API client so that the personal token creation isn't
# taken in account in the actual resource creation timing.
client.personal_access_token
end
end
private
attr_writer :api_resource, :api_response
def resource_web_url(resource)
resource.fetch(:web_url) do
raise ResourceURLMissingError, "API resource for #{self.class.name} does not expose a `web_url` property: `#{resource}`."
end
end
def api_get
url = Runtime::API::Request.new(api_client, api_get_path).url
response = get(url)
unless response.code == HTTP_STATUS_OK
raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`."
end
process_api_response(parse_body(response))
end
def api_post
response = post(
Runtime::API::Request.new(api_client, api_post_path).url,
api_post_body)
unless response.code == HTTP_STATUS_CREATED
raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
end
process_api_response(parse_body(response))
end
def api_client
@api_client ||= begin
Runtime::API::Client.new(:gitlab, is_new_session: !current_url.start_with?('http'))
end
end
def parse_body(response)
JSON.parse(response.body, symbolize_names: true)
end
def process_api_response(parsed_response)
self.api_response = parsed_response
self.api_resource = transform_api_resource(parsed_response.deep_dup)
end
def transform_api_resource(resource)
resource
end
end
end
end
# frozen_string_literal: true
require 'forwardable' require 'forwardable'
require 'capybara/dsl'
module QA module QA
module Factory module Factory
class Base class Base
extend SingleForwardable extend SingleForwardable
include ApiFabricator
extend Capybara::DSL
def_delegators :evaluator, :dependency, :dependencies def_delegators :evaluator, :dependency, :dependencies
def_delegators :evaluator, :product, :attributes def_delegators :evaluator, :product, :attributes
...@@ -12,46 +17,96 @@ module QA ...@@ -12,46 +17,96 @@ module QA
raise NotImplementedError raise NotImplementedError
end end
def self.fabricate!(*args) def self.fabricate!(*args, &prepare_block)
new.tap do |factory| fabricate_via_api!(*args, &prepare_block)
yield factory if block_given? rescue NotImplementedError
fabricate_via_browser_ui!(*args, &prepare_block)
end
dependencies.each do |name, signature| def self.fabricate_via_browser_ui!(*args, &prepare_block)
Factory::Dependency.new(name, factory, signature).build! options = args.extract_options!
end factory = options.fetch(:factory) { new }
parents = options.fetch(:parents) { [] }
do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) }
current_url
end
end
def self.fabricate_via_api!(*args, &prepare_block)
options = args.extract_options!
factory = options.fetch(:factory) { new }
parents = options.fetch(:parents) { [] }
raise NotImplementedError unless factory.api_support?
factory.eager_load_api_client!
do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! }
end
end
def self.do_fabricate!(factory:, prepare_block:, parents: [])
prepare_block.call(factory) if prepare_block
dependencies.each do |signature|
Factory::Dependency.new(factory, signature).build!(parents: parents + [self])
end
resource_web_url = yield
Factory::Product.populate!(factory, resource_web_url)
end
private_class_method :do_fabricate!
def self.log_fabrication(method, factory, parents, args)
return yield unless Runtime::Env.verbose?
factory.fabricate!(*args) start = Time.now
prefix = "==#{'=' * parents.size}>"
msg = [prefix]
msg << "Built a #{name}"
msg << "as a dependency of #{parents.last}" if parents.any?
msg << "via #{method} with args #{args}"
break Factory::Product.populate!(factory) yield.tap do
msg << "in #{Time.now - start} seconds"
puts msg.join(' ')
puts if parents.empty?
end end
end end
private_class_method :log_fabrication
def self.evaluator def self.evaluator
@evaluator ||= Factory::Base::DSL.new(self) @evaluator ||= Factory::Base::DSL.new(self)
end end
private_class_method :evaluator
class DSL class DSL
attr_reader :dependencies, :attributes attr_reader :dependencies, :attributes
def initialize(base) def initialize(base)
@base = base @base = base
@dependencies = {} @dependencies = []
@attributes = {} @attributes = []
end end
def dependency(factory, as:, &block) def dependency(factory, as:, &block)
as.tap do |name| as.tap do |name|
@base.class_eval { attr_accessor name } @base.class_eval { attr_accessor name }
Dependency::Signature.new(factory, block).tap do |signature| Dependency::Signature.new(name, factory, block).tap do |signature|
@dependencies.store(name, signature) @dependencies << signature
end end
end end
end end
def product(attribute, &block) def product(attribute, &block)
Product::Attribute.new(attribute, block).tap do |signature| Product::Attribute.new(attribute, block).tap do |signature|
@attributes.store(attribute, signature) @attributes << signature
end end
end end
end end
......
module QA module QA
module Factory module Factory
class Dependency class Dependency
Signature = Struct.new(:factory, :block) Signature = Struct.new(:name, :factory, :block)
def initialize(name, factory, signature) def initialize(caller_factory, dependency_signature)
@name = name @caller_factory = caller_factory
@factory = factory @dependency_signature = dependency_signature
@signature = signature
end end
def overridden? def overridden?
!!@factory.public_send(@name) !!@caller_factory.public_send(@dependency_signature.name)
end end
def build! def build!(parents: [])
return if overridden? return if overridden?
Builder.new(@signature, @factory).fabricate!.tap do |product| dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
@factory.public_send("#{@name}=", product) @dependency_signature.block&.call(factory, @caller_factory)
end
end
class Builder
def initialize(signature, caller_factory)
@factory = signature.factory
@block = signature.block
@caller_factory = caller_factory
end end
def fabricate! dependency.tap do |dependency|
@factory.fabricate! do |factory| @caller_factory.public_send("#{@dependency_signature.name}=", dependency)
@block&.call(factory, @caller_factory)
end
end end
end end
end end
......
...@@ -5,26 +5,46 @@ module QA ...@@ -5,26 +5,46 @@ module QA
class Product class Product
include Capybara::DSL include Capybara::DSL
NoValueError = Class.new(RuntimeError)
attr_reader :factory, :web_url
Attribute = Struct.new(:name, :block) Attribute = Struct.new(:name, :block)
def initialize def initialize(factory, web_url)
@location = current_url @factory = factory
@web_url = web_url
populate_attributes!
end end
def visit! def visit!
visit @location visit(web_url)
end
def self.populate!(factory, web_url)
new(factory, web_url)
end end
def self.populate!(factory) private
new.tap do |product|
factory.class.attributes.each_value do |attribute| def populate_attributes!
product.instance_exec(factory, attribute.block) do |factory, block| factory.class.attributes.each do |attribute|
value = block.call(factory) instance_exec(factory, attribute.block) do |factory, block|
product.define_singleton_method(attribute.name) { value } value = attribute_value(attribute, block)
end
raise NoValueError, "No value was computed for product #{attribute.name} of factory #{factory.class.name}." unless value
define_singleton_method(attribute.name) { value }
end end
end end
end end
def attribute_value(attribute, block)
factory.api_resource&.dig(attribute.name) ||
(block && block.call(factory)) ||
(factory.respond_to?(attribute.name) && factory.public_send(attribute.name))
end
end end
end end
end end
...@@ -7,13 +7,8 @@ module QA ...@@ -7,13 +7,8 @@ module QA
project.description = 'Project with repository' project.description = 'Project with repository'
end end
product :output do |factory| product :output
factory.output product :project
end
product :project do |factory|
factory.project
end
def initialize def initialize
@file_name = 'file.txt' @file_name = 'file.txt'
......
...@@ -11,7 +11,7 @@ module QA ...@@ -11,7 +11,7 @@ module QA
end end
end end
product(:user) { |factory| factory.user } product :user
def visit_project_with_retry def visit_project_with_retry
# The user intermittently fails to stay signed in after visiting the # The user intermittently fails to stay signed in after visiting the
......
...@@ -6,6 +6,10 @@ module QA ...@@ -6,6 +6,10 @@ module QA
dependency Factory::Resource::Sandbox, as: :sandbox dependency Factory::Resource::Sandbox, as: :sandbox
product :id do
true # We don't retrieve the Group ID when using the Browser UI
end
def initialize def initialize
@path = Runtime::Namespace.name @path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}" @description = "QA test run at #{Runtime::Namespace.time}"
...@@ -35,6 +39,29 @@ module QA ...@@ -35,6 +39,29 @@ module QA
end end
end end
end end
def fabricate_via_api!
resource_web_url(api_get)
rescue ResourceNotFoundError
super
end
def api_get_path
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
end
def api_post_path
'/groups'
end
def api_post_body
{
parent_id: sandbox.id,
path: path,
name: path,
visibility: 'public'
}
end
end end
end end
end end
......
...@@ -2,16 +2,15 @@ module QA ...@@ -2,16 +2,15 @@ module QA
module Factory module Factory
module Resource module Resource
class Issue < Factory::Base class Issue < Factory::Base
attr_writer :title, :description, :project attr_accessor :title, :description, :project
dependency Factory::Resource::Project, as: :project do |project| dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues' project.name = 'project-for-issues'
project.description = 'project for adding issues' project.description = 'project for adding issues'
end end
product :title do product :project
Page::Project::Issue::Show.act { issue_title } product :title
end
def fabricate! def fabricate!
project.visit! project.visit!
......
...@@ -12,13 +12,8 @@ module QA ...@@ -12,13 +12,8 @@ module QA
:milestone, :milestone,
:labels :labels
product :project do |factory| product :project
factory.project product :source_branch
end
product :source_branch do |factory|
factory.source_branch
end
dependency Factory::Resource::Project, as: :project do |project| dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-merge-request' project.name = 'project-with-merge-request'
......
...@@ -4,14 +4,13 @@ module QA ...@@ -4,14 +4,13 @@ module QA
module Factory module Factory
module Resource module Resource
class Project < Factory::Base class Project < Factory::Base
attr_writer :description attr_accessor :description
attr_reader :name attr_reader :name
dependency Factory::Resource::Group, as: :group dependency Factory::Resource::Group, as: :group
product :name do |factory| product :group
factory.name product :name
end
product :repository_ssh_location do product :repository_ssh_location do
Page::Project::Show.act do Page::Project::Show.act do
...@@ -48,6 +47,32 @@ module QA ...@@ -48,6 +47,32 @@ module QA
page.create_new_project page.create_new_project
end end
end end
def api_get_path
"/projects/#{name}"
end
def api_post_path
'/projects'
end
def api_post_body
{
namespace_id: group.id,
path: name,
name: name,
description: description,
visibility: 'public'
}
end
private
def transform_api_resource(resource)
resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo])
resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo])
resource
end
end end
end end
end end
......
...@@ -8,9 +8,7 @@ module QA ...@@ -8,9 +8,7 @@ module QA
dependency Factory::Resource::Group, as: :group dependency Factory::Resource::Group, as: :group
product :name do |factory| product :name
factory.name
end
def fabricate! def fabricate!
group.visit! group.visit!
......
...@@ -7,7 +7,7 @@ module QA ...@@ -7,7 +7,7 @@ module QA
dependency Factory::Resource::Project, as: :project dependency Factory::Resource::Project, as: :project
product(:title) { |factory| factory.title } product :title
def title=(title) def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}" @title = "#{title}-#{SecureRandom.hex(4)}"
......
...@@ -6,21 +6,28 @@ module QA ...@@ -6,21 +6,28 @@ module QA
# creating it if it doesn't yet exist. # creating it if it doesn't yet exist.
# #
class Sandbox < Factory::Base class Sandbox < Factory::Base
attr_reader :path
product :id do
true # We don't retrieve the Group ID when using the Browser UI
end
product :path
def initialize def initialize
@name = Runtime::Namespace.sandbox_name @path = Runtime::Namespace.sandbox_name
end end
def fabricate! def fabricate!
Page::Main::Menu.act { go_to_groups } Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page| Page::Dashboard::Groups.perform do |page|
if page.has_group?(@name) if page.has_group?(path)
page.go_to_group(@name) page.go_to_group(path)
else else
page.go_to_new_group page.go_to_new_group
Page::Group::New.perform do |group| Page::Group::New.perform do |group|
group.set_path(@name) group.set_path(path)
group.set_description('GitLab QA Sandbox Group') group.set_description('GitLab QA Sandbox Group')
group.set_visibility('Public') group.set_visibility('Public')
group.create group.create
...@@ -28,6 +35,28 @@ module QA ...@@ -28,6 +35,28 @@ module QA
end end
end end
end end
def fabricate_via_api!
resource_web_url(api_get)
rescue ResourceNotFoundError
super
end
def api_get_path
"/groups/#{path}"
end
def api_post_path
'/groups'
end
def api_post_body
{
path: path,
name: path,
visibility: 'public'
}
end
end end
end end
end end
......
...@@ -10,17 +10,9 @@ module QA ...@@ -10,17 +10,9 @@ module QA
attr_reader :private_key, :public_key, :fingerprint attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint def_delegators :key, :private_key, :public_key, :fingerprint
product :private_key do |factory| product :private_key
factory.private_key product :title
end product :fingerprint
product :title do |factory|
factory.title
end
product :fingerprint do |factory|
factory.fingerprint
end
def key def key
@key ||= Runtime::Key::RSA.new @key ||= Runtime::Key::RSA.new
......
...@@ -31,10 +31,10 @@ module QA ...@@ -31,10 +31,10 @@ module QA
defined?(@username) && defined?(@password) defined?(@username) && defined?(@password)
end end
product(:name) { |factory| factory.name } product :name
product(:username) { |factory| factory.username } product :username
product(:email) { |factory| factory.email } product :email
product(:password) { |factory| factory.password } product :password
def fabricate! def fabricate!
# Don't try to log-out if we're not logged-in # Don't try to log-out if we're not logged-in
......
...@@ -10,13 +10,16 @@ module QA ...@@ -10,13 +10,16 @@ module QA
end end
def fabricate! def fabricate!
Page::Project::Menu.act { click_wiki } project.visit!
Page::Project::Wiki::New.perform do |page|
page.go_to_create_first_page Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
page.set_title(@title)
page.set_content(@content) Page::Project::Wiki::New.perform do |wiki_new|
page.set_message(@message) wiki_new.go_to_create_first_page
page.create_new_page wiki_new.set_title(@title)
wiki_new.set_content(@content)
wiki_new.set_message(@message)
wiki_new.create_new_page
end end
end end
end end
......
...@@ -131,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack ...@@ -131,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack
(internal, GitLab Team only). (internal, GitLab Team only).
If you are not a Team Member, and you still need help to contribute, please If you are not a Team Member, and you still need help to contribute, please
open an issue in GitLab QA issue tracker. open an issue in GitLab CE issue tracker with the `~QA` label.
...@@ -6,33 +6,34 @@ module QA ...@@ -6,33 +6,34 @@ module QA
class Client class Client
attr_reader :address attr_reader :address
def initialize(address = :gitlab, personal_access_token: nil) def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true)
@address = address @address = address
@personal_access_token = personal_access_token @personal_access_token = personal_access_token
@is_new_session = is_new_session
end end
def personal_access_token def personal_access_token
@personal_access_token ||= get_personal_access_token @personal_access_token ||= begin
end # you can set the environment variable PERSONAL_ACCESS_TOKEN
# to use a specific access token rather than create one from the UI
def get_personal_access_token Runtime::Env.personal_access_token ||= create_personal_access_token
# you can set the environment variable PERSONAL_ACCESS_TOKEN
# to use a specific access token rather than create one from the UI
if Runtime::Env.personal_access_token
Runtime::Env.personal_access_token
else
create_personal_access_token
end end
end end
private private
def create_personal_access_token def create_personal_access_token
Runtime::Browser.visit(@address, Page::Main::Login) do if @is_new_session
Page::Main::Login.act { sign_in_using_credentials } Runtime::Browser.visit(@address, Page::Main::Login) { do_create_personal_access_token }
Factory::Resource::PersonalAccessToken.fabricate!.access_token else
do_create_personal_access_token
end end
end end
def do_create_personal_access_token
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::PersonalAccessToken.fabricate!.access_token
end
end end
end end
end end
......
...@@ -3,6 +3,12 @@ module QA ...@@ -3,6 +3,12 @@ module QA
module Env module Env
extend self extend self
attr_writer :personal_access_token
def verbose?
enabled?(ENV['VERBOSE'], default: false)
end
# set to 'false' to have Chrome run visibly instead of headless # set to 'false' to have Chrome run visibly instead of headless
def chrome_headless? def chrome_headless?
enabled?(ENV['CHROME_HEADLESS']) enabled?(ENV['CHROME_HEADLESS'])
...@@ -22,7 +28,7 @@ module QA ...@@ -22,7 +28,7 @@ module QA
# specifies token that can be used for the api # specifies token that can be used for the api
def personal_access_token def personal_access_token
ENV['PERSONAL_ACCESS_TOKEN'] @personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN']
end end
def user_username def user_username
...@@ -42,7 +48,7 @@ module QA ...@@ -42,7 +48,7 @@ module QA
end end
def forker? def forker?
forker_username && forker_password !!(forker_username && forker_password)
end end
def forker_username def forker_username
......
...@@ -11,9 +11,10 @@ module QA ...@@ -11,9 +11,10 @@ module QA
Page::Main::Menu.perform { |main| main.sign_out } Page::Main::Menu.perform { |main| main.sign_out }
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Project.fabricate! do |resource| project = Factory::Resource::Project.fabricate! do |resource|
resource.name = 'add-member-project' resource.name = 'add-member-project'
end end
project.visit!
Page::Project::Menu.act { click_members_settings } Page::Project::Menu.act { click_members_settings }
Page::Project::Settings::Members.perform do |page| Page::Project::Settings::Members.perform do |page|
......
...@@ -7,17 +7,15 @@ module QA ...@@ -7,17 +7,15 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
created_project = Factory::Resource::Project.fabricate! do |project| created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project|
project.name = 'awesome-project' project.name = 'awesome-project'
project.description = 'create awesome project test' project.description = 'create awesome project test'
end end
expect(created_project.name).to match /^awesome-project-\h{16}$/ expect(page).to have_content(created_project.name)
expect(page).to have_content( expect(page).to have_content(
/Project \S?awesome-project\S+ was successfully created/ /Project \S?awesome-project\S+ was successfully created/
) )
expect(page).to have_content('create awesome project test') expect(page).to have_content('create awesome project test')
expect(page).to have_content('The repository for this project is empty') expect(page).to have_content('The repository for this project is empty')
end end
......
...@@ -10,6 +10,7 @@ module QA ...@@ -10,6 +10,7 @@ module QA
project = Factory::Resource::Project.fabricate! do |project| project = Factory::Resource::Project.fabricate! do |project|
project.name = "only-fast-forward" project.name = "only-fast-forward"
end end
project.visit!
Page::Project::Menu.act { go_to_settings } Page::Project::Menu.act { go_to_settings }
Page::Project::Settings::MergeRequest.act { enable_ff_only } Page::Project::Settings::MergeRequest.act { enable_ff_only }
......
...@@ -14,10 +14,11 @@ module QA ...@@ -14,10 +14,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Project.fabricate! do |scenario| project = Factory::Resource::Project.fabricate! do |scenario|
scenario.name = 'project-with-code' scenario.name = 'project-with-code'
scenario.description = 'project for git clone tests' scenario.description = 'project for git clone tests'
end end
project.visit!
Git::Repository.perform do |repository| Git::Repository.perform do |repository|
repository.uri = location.uri repository.uri = location.uri
......
...@@ -17,6 +17,7 @@ module QA ...@@ -17,6 +17,7 @@ module QA
project.name = 'file-template-project' project.name = 'file-template-project'
project.description = 'Add file templates via the Web IDE' project.description = 'Add file templates via the Web IDE'
end end
@project.visit!
# Add a file via the regular Files view because the Web IDE isn't # Add a file via the regular Files view because the Web IDE isn't
# available unless there is a file present # available unless there is a file present
......
# frozen_string_literal: true
describe QA::Factory::ApiFabricator do
let(:factory_without_api_support) do
Class.new do
def self.name
'FooBarFactory'
end
end
end
let(:factory_with_api_support) do
Class.new do
def self.name
'FooBarFactory'
end
def api_get_path
'/foo'
end
def api_post_path
'/bar'
end
def api_post_body
{ name: 'John Doe' }
end
end
end
before do
allow(subject).to receive(:current_url).and_return('')
end
subject { factory.tap { |f| f.include(described_class) }.new }
describe '#api_support?' do
let(:api_client) { spy('Runtime::API::Client') }
let(:api_client_instance) { double('API Client') }
context 'when factory does not support fabrication via the API' do
let(:factory) { factory_without_api_support }
it 'returns false' do
expect(subject).not_to be_api_support
end
end
context 'when factory supports fabrication via the API' do
let(:factory) { factory_with_api_support }
it 'returns false' do
expect(subject).to be_api_support
end
end
end
describe '#fabricate_via_api!' do
let(:api_client) { spy('Runtime::API::Client') }
let(:api_client_instance) { double('API Client') }
before do
stub_const('QA::Runtime::API::Client', api_client)
allow(api_client).to receive(:new).and_return(api_client_instance)
allow(api_client_instance).to receive(:personal_access_token).and_return('foo')
end
context 'when factory does not support fabrication via the API' do
let(:factory) { factory_without_api_support }
it 'raises a NotImplementedError exception' do
expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Factory FooBarFactory does not support fabrication via the API!")
end
end
context 'when factory supports fabrication via the API' do
let(:factory) { factory_with_api_support }
let(:api_request) { spy('Runtime::API::Request') }
let(:resource_web_url) { 'http://example.org/api/v4/foo' }
let(:resource) { { id: 1, name: 'John Doe', web_url: resource_web_url } }
let(:raw_post) { double('Raw POST response', code: 201, body: resource.to_json) }
before do
stub_const('QA::Runtime::API::Request', api_request)
allow(api_request).to receive(:new).and_return(double(url: resource_web_url))
end
context 'when creating a resource' do
before do
allow(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
end
it 'returns the resource URL' do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
expect(subject.fabricate_via_api!).to eq(resource_web_url)
end
it 'populates api_resource with the resource' do
subject.fabricate_via_api!
expect(subject.api_resource).to eq(resource)
end
context 'when the POST fails' do
let(:post_response) { { error: "Name already taken." } }
let(:raw_post) { double('Raw POST response', code: 400, body: post_response.to_json) }
it 'raises a ResourceFabricationFailedError exception' do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarFactory using the API failed (400) with `#{raw_post}`.")
expect(subject.api_resource).to be_nil
end
end
end
context '#transform_api_resource' do
let(:factory) do
Class.new do
def self.name
'FooBarFactory'
end
def api_get_path
'/foo'
end
def api_post_path
'/bar'
end
def api_post_body
{ name: 'John Doe' }
end
def transform_api_resource(resource)
resource[:new] = 'foobar'
resource
end
end
end
let(:resource) { { existing: 'foo', web_url: resource_web_url } }
let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } }
it 'transforms the resource' do
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
expect(subject).to receive(:transform_api_resource).with(resource).and_return(transformed_resource)
subject.fabricate_via_api!
end
end
end
end
end
# frozen_string_literal: true
describe QA::Factory::Base do describe QA::Factory::Base do
include Support::StubENV
let(:factory) { spy('factory') } let(:factory) { spy('factory') }
let(:product) { spy('product') } let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
describe '.fabricate!' do shared_context 'fabrication context' do
subject { Class.new(described_class) } subject do
Class.new(described_class) do
def self.name
'MyFactory'
end
end
end
before do before do
allow(QA::Factory::Product).to receive(:new).and_return(product) allow(subject).to receive(:current_url).and_return(product_location)
allow(QA::Factory::Product).to receive(:populate!).and_return(product) allow(subject).to receive(:new).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).with(factory, product_location).and_return(product)
end end
end
it 'instantiates the factory and calls factory method' do shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
expect(subject).to receive(:new).and_return(factory) let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called }
subject.fabricate!('something') it 'yields factory before calling factory method' do
expect(factory).to receive(:something!).ordered
expect(factory).to receive(fabrication_method_used).ordered.and_return(product_location)
expect(factory).to have_received(:fabricate!).with('something') subject.public_send(fabrication_method_called, factory: factory) do |factory|
factory.something!
end
end end
it 'returns fabrication product' do it 'does not log the factory and build method when VERBOSE=false' do
allow(subject).to receive(:new).and_return(factory) stub_env('VERBOSE', 'false')
expect(factory).to receive(fabrication_method_used).and_return(product_location)
result = subject.fabricate!('something') expect { subject.public_send(fabrication_method_called, 'something', factory: factory) }
.not_to output.to_stdout
end
end
describe '.fabricate!' do
context 'when factory does not support fabrication via the API' do
before do
expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError)
end
expect(result).to eq product it 'calls .fabricate_via_browser_ui!' do
expect(described_class).to receive(:fabricate_via_browser_ui!)
described_class.fabricate!
end
end end
it 'yields factory before calling factory method' do context 'when factory supports fabrication via the API' do
allow(subject).to receive(:new).and_return(factory) it 'calls .fabricate_via_browser_ui!' do
expect(described_class).to receive(:fabricate_via_api!)
subject.fabricate! do |factory| described_class.fabricate!
factory.something!
end end
end
end
describe '.fabricate_via_api!' do
include_context 'fabrication context'
it_behaves_like 'fabrication method', :fabricate_via_api!
it 'instantiates the factory, calls factory method returns fabrication product' do
expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
expect(factory).to have_received(:something!).ordered result = subject.fabricate_via_api!(factory: factory, parents: [])
expect(factory).to have_received(:fabricate!).ordered
expect(result).to eq(product)
end
it 'logs the factory and build method when VERBOSE=true' do
stub_env('VERBOSE', 'true')
expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
expect { subject.fabricate_via_api!(factory: factory, parents: []) }
.to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/)
.to_stdout
end
end
describe '.fabricate_via_browser_ui!' do
include_context 'fabrication context'
it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate!
it 'instantiates the factory and calls factory method' do
subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
expect(factory).to have_received(:fabricate!).with('something')
end
it 'returns fabrication product' do
result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
expect(result).to eq(product)
end
it 'logs the factory and build method when VERBOSE=true' do
stub_env('VERBOSE', 'true')
expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) }
.to output(/==> Built a MyFactory via browser_ui with args \["something"\] in [\d\w\.\-]+/)
.to_stdout
end end
end end
...@@ -75,9 +152,9 @@ describe QA::Factory::Base do ...@@ -75,9 +152,9 @@ describe QA::Factory::Base do
stub_const('Some::MyDependency', dependency) stub_const('Some::MyDependency', dependency)
allow(subject).to receive(:new).and_return(instance) allow(subject).to receive(:new).and_return(instance)
allow(subject).to receive(:current_url).and_return(product_location)
allow(instance).to receive(:mydep).and_return(nil) allow(instance).to receive(:mydep).and_return(nil)
allow(QA::Factory::Product).to receive(:new) expect(QA::Factory::Product).to receive(:populate!)
allow(QA::Factory::Product).to receive(:populate!)
end end
it 'builds all dependencies first' do it 'builds all dependencies first' do
...@@ -89,44 +166,22 @@ describe QA::Factory::Base do ...@@ -89,44 +166,22 @@ describe QA::Factory::Base do
end end
describe '.product' do describe '.product' do
include_context 'fabrication context'
subject do subject do
Class.new(described_class) do Class.new(described_class) do
def fabricate! def fabricate!
"any" "any"
end end
# Defined only to be stubbed product :token
def self.find_page
end
product :token do
find_page.do_something_on_page!
'resulting value'
end
end end
end end
it 'appends new product attribute' do it 'appends new product attribute' do
expect(subject.attributes).to be_one expect(subject.attributes).to be_one
expect(subject.attributes).to have_key(:token) expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
end expect(subject.attributes[0].name).to eq(:token)
describe 'populating fabrication product with data' do
let(:page) { spy('page') }
before do
allow(factory).to receive(:class).and_return(subject)
allow(QA::Factory::Product).to receive(:new).and_return(product)
allow(product).to receive(:page).and_return(page)
allow(subject).to receive(:find_page).and_return(page)
end
it 'populates product after fabrication' do
subject.fabricate!
expect(product.token).to eq 'resulting value'
expect(page).to have_received(:do_something_on_page!)
end
end end
end end
end end
...@@ -4,11 +4,11 @@ describe QA::Factory::Dependency do ...@@ -4,11 +4,11 @@ describe QA::Factory::Dependency do
let(:block) { spy('block') } let(:block) { spy('block') }
let(:signature) do let(:signature) do
double('signature', factory: dependency, block: block) double('signature', name: :mydep, factory: dependency, block: block)
end end
subject do subject do
described_class.new(:mydep, factory, signature) described_class.new(factory, signature)
end end
describe '#overridden?' do describe '#overridden?' do
...@@ -55,16 +55,23 @@ describe QA::Factory::Dependency do ...@@ -55,16 +55,23 @@ describe QA::Factory::Dependency do
expect(factory).to have_received(:mydep=).with(dependency) expect(factory).to have_received(:mydep=).with(dependency)
end end
context 'when receives a caller factory as block argument' do it 'calls given block with dependency factory and caller factory' do
let(:dependency) { QA::Factory::Base } expect(dependency).to receive(:fabricate!).and_yield(dependency)
it 'calls given block with dependency factory and caller factory' do subject.build!
allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any')) expect(block).to have_received(:call).with(dependency, factory)
end
context 'with no block given' do
let(:signature) do
double('signature', name: :mydep, factory: dependency, block: nil)
end
it 'does not error' do
subject.build! subject.build!
expect(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory) expect(dependency).to have_received(:fabricate!)
end end
end end
end end
......
describe QA::Factory::Product do describe QA::Factory::Product do
let(:factory) do let(:factory) do
QA::Factory::Base.new Class.new(QA::Factory::Base) do
end def foo
'bar'
let(:attributes) do end
{ test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) } end.new
end end
let(:product) { spy('product') } let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
before do subject { described_class.new(factory, product_location) }
allow(QA::Factory::Base).to receive(:attributes).and_return(attributes)
end
describe '.populate!' do describe '.populate!' do
it 'returns a fabrication product and define factory attributes as its methods' do before do
expect(described_class).to receive(:new).and_return(product) expect(factory.class).to receive(:attributes).and_return(attributes)
end
context 'when the product attribute is populated via a block' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:test, proc { 'returned' })]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = described_class.populate!(factory, product_location)
expect(result).to be_a(described_class)
expect(result.test).to eq('returned')
end
end
context 'when the product attribute is populated via the api' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:test)]
end
result = described_class.populate!(factory) do |instance| it 'returns a fabrication product and defines factory attributes as its methods' do
instance.something = 'string' expect(factory).to receive(:api_resource).and_return({ test: 'returned' })
result = described_class.populate!(factory, product_location)
expect(result).to be_a(described_class)
expect(result.test).to eq('returned')
end end
end
expect(result).to be product context 'when the product attribute is populated via a factory attribute' do
expect(result.test).to eq('returned') let(:attributes) do
[QA::Factory::Product::Attribute.new(:foo)]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = described_class.populate!(factory, product_location)
expect(result).to be_a(described_class)
expect(result.foo).to eq('bar')
end
end
context 'when the product attribute has no value' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:bar)]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
expect { described_class.populate!(factory, product_location) }
.to raise_error(described_class::NoValueError, "No value was computed for product bar of factory #{factory.class.name}.")
end
end end
end end
describe '.visit!' do describe '.visit!' do
it 'makes it possible to visit fabrication product' do it 'makes it possible to visit fabrication product' do
allow_any_instance_of(described_class)
.to receive(:current_url).and_return('some url')
allow_any_instance_of(described_class) allow_any_instance_of(described_class)
.to receive(:visit).and_return('visited some url') .to receive(:visit).and_return('visited some url')
......
...@@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do ...@@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do
end end
end end
describe '#get_personal_access_token' do describe '#personal_access_token' do
it 'returns specified token from env' do context 'when QA::Runtime::Env.personal_access_token is present' do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') before do
allow(QA::Runtime::Env).to receive(:personal_access_token).and_return('a_token')
end
expect(described_class.new.get_personal_access_token).to eq 'a_token' it 'returns specified token from env' do
expect(described_class.new.personal_access_token).to eq 'a_token'
end
end end
it 'returns a created token' do context 'when QA::Runtime::Env.personal_access_token is nil' do
allow_any_instance_of(described_class) before do
.to receive(:create_personal_access_token).and_return('created_token') allow(QA::Runtime::Env).to receive(:personal_access_token).and_return(nil)
end
expect(described_class.new.get_personal_access_token).to eq 'created_token' it 'returns a created token' do
expect(subject).to receive(:create_personal_access_token).and_return('created_token')
expect(subject.personal_access_token).to eq 'created_token'
end
end end
end end
end end
describe QA::Runtime::API::Request do describe QA::Runtime::API::Request do
include Support::StubENV let(:client) { QA::Runtime::API::Client.new('http://example.com') }
let(:request) { described_class.new(client, '/users') }
before do before do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') allow(client).to receive(:personal_access_token).and_return('a_token')
end end
let(:client) { QA::Runtime::API::Client.new('http://example.com') }
let(:request) { described_class.new(client, '/users') }
describe '#url' do describe '#url' do
it 'returns the full api request url' do it 'returns the full API request url' do
expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token' expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
end end
context 'when oauth_access_token is passed in the query string' do
let(:request) { described_class.new(client, '/users', { oauth_access_token: 'foo' }) }
it 'does not adds a private_token query string' do
expect(request.url).to eq 'http://example.com/api/v4/users?oauth_access_token=foo'
end
end
end end
describe '#request_path' do describe '#request_path' do
......
...@@ -34,6 +34,10 @@ describe QA::Runtime::Env do ...@@ -34,6 +34,10 @@ describe QA::Runtime::Env do
end end
end end
describe '.verbose?' do
it_behaves_like 'boolean method', :verbose?, 'VERBOSE', false
end
describe '.signup_disabled?' do describe '.signup_disabled?' do
it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false
end end
...@@ -64,7 +68,54 @@ describe QA::Runtime::Env do ...@@ -64,7 +68,54 @@ describe QA::Runtime::Env do
end end
end end
describe '.personal_access_token' do
around do |example|
described_class.instance_variable_set(:@personal_access_token, nil)
example.run
described_class.instance_variable_set(:@personal_access_token, nil)
end
context 'when PERSONAL_ACCESS_TOKEN is set' do
before do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
end
it 'returns specified token from env' do
expect(described_class.personal_access_token).to eq 'a_token'
end
end
context 'when @personal_access_token is set' do
before do
described_class.personal_access_token = 'another_token'
end
it 'returns the instance variable value' do
expect(described_class.personal_access_token).to eq 'another_token'
end
end
end
describe '.personal_access_token=' do
around do |example|
described_class.instance_variable_set(:@personal_access_token, nil)
example.run
described_class.instance_variable_set(:@personal_access_token, nil)
end
it 'saves the token' do
described_class.personal_access_token = 'a_token'
expect(described_class.personal_access_token).to eq 'a_token'
end
end
describe '.forker?' do describe '.forker?' do
before do
stub_env('GITLAB_FORKER_USERNAME', nil)
stub_env('GITLAB_FORKER_PASSWORD', nil)
end
it 'returns false if no forker credentials are defined' do it 'returns false if no forker credentials are defined' do
expect(described_class).not_to be_forker expect(described_class).not_to be_forker
end end
......
...@@ -22,20 +22,18 @@ describe Projects::WikisController do ...@@ -22,20 +22,18 @@ describe Projects::WikisController do
subject { get :show, namespace_id: project.namespace, project_id: project, id: wiki_title } subject { get :show, namespace_id: project.namespace, project_id: project, id: wiki_title }
context 'when page content encoding is invalid' do it 'limits the retrieved pages for the sidebar' do
it 'limits the retrieved pages for the sidebar' do expect(controller).to receive(:load_wiki).and_return(project_wiki)
expect(controller).to receive(:load_wiki).and_return(project_wiki)
# empty? call # empty? call
expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original
# Sidebar entries # Sidebar entries
expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original
subject subject
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(response.body).to include(wiki_title) expect(response.body).to include(wiki_title)
end
end end
context 'when page content encoding is invalid' do context 'when page content encoding is invalid' do
...@@ -48,6 +46,42 @@ describe Projects::WikisController do ...@@ -48,6 +46,42 @@ describe Projects::WikisController do
expect(flash[:notice]).to eq 'The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.' expect(flash[:notice]).to eq 'The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.'
end end
end end
context 'when page is a file' do
include WikiHelpers
let(:path) { upload_file_to_wiki(project, user, file_name) }
before do
subject
end
subject { get :show, namespace_id: project.namespace, project_id: project, id: path }
context 'when file is an image' do
let(:file_name) { 'dk.png' }
it 'renders the content inline' do
expect(response.headers['Content-Disposition']).to match(/^inline/)
end
context 'when file is a svg' do
let(:file_name) { 'unsanitized.svg' }
it 'renders the content as an attachment' do
expect(response.headers['Content-Disposition']).to match(/^attachment/)
end
end
end
context 'when file is a pdf' do
let(:file_name) { 'git-cheat-sheet.pdf' }
it 'sets the content type to application/octet-stream' do
expect(response.headers['Content-Type']).to eq 'application/octet-stream'
end
end
end
end end
describe 'POST #preview_markdown' do describe 'POST #preview_markdown' do
......
...@@ -54,7 +54,7 @@ describe 'User views empty wiki' do ...@@ -54,7 +54,7 @@ describe 'User views empty wiki' do
it_behaves_like 'empty wiki and non-accessible issues' it_behaves_like 'empty wiki and non-accessible issues'
end end
context 'when user is logged in and a memeber' do context 'when user is logged in and a member' do
let(:project) { create(:project, :public, :wiki_repo) } let(:project) { create(:project, :public, :wiki_repo) }
before do before do
......
...@@ -2,12 +2,15 @@ require 'spec_helper' ...@@ -2,12 +2,15 @@ require 'spec_helper'
describe 'User views a wiki page' do describe 'User views a wiki page' do
shared_examples 'wiki page user view' do shared_examples 'wiki page user view' do
include WikiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:path) { 'image.png' }
let(:wiki_page) do let(:wiki_page) do
create(:wiki_page, create(:wiki_page,
wiki: project.wiki, wiki: project.wiki,
attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' }) attrs: { title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})" })
end end
before do before do
...@@ -82,33 +85,26 @@ describe 'User views a wiki page' do ...@@ -82,33 +85,26 @@ describe 'User views a wiki page' do
expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize) expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
end end
it 'shows a file stored in a page' do context 'shows a file stored in a page' do
raw_file = Gitlab::GitalyClient::WikiFile.new( let(:path) { upload_file_to_wiki(project, user, 'dk.png') }
mime_type: 'image/jpeg',
name: 'images/image.jpg',
path: 'images/image.jpg',
raw_data: ''
)
wiki_file = Gitlab::Git::WikiFile.new(raw_file)
allow(wiki_file).to receive(:mime_type).and_return('image/jpeg')
allow_any_instance_of(ProjectWiki).to receive(:find_file).with('image.jpg', nil).and_return(wiki_file)
expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/image.jpg']") it do
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/#{path}']")
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}")
click_on('image') click_on('image')
expect(current_path).to match('wikis/image.jpg') expect(current_path).to match("wikis/#{path}")
expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
end
end end
it 'shows the creation page if file does not exist' do it 'shows the creation page if file does not exist' do
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/#{path}")
click_on('image') click_on('image')
expect(current_path).to match('wikis/image.jpg') expect(current_path).to match("wikis/#{path}")
expect(page).to have_content('New Wiki Page') expect(page).to have_content('New Wiki Page')
expect(page).to have_content('Create page') expect(page).to have_content('Create page')
end end
......
...@@ -112,6 +112,17 @@ describe('Application Row', () => { ...@@ -112,6 +112,17 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true); expect(vm.installButtonDisabled).toEqual(true);
}); });
it('has disabled "Installed" when APPLICATION_STATUS.UPDATING', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING,
});
expect(vm.installButtonLabel).toEqual('Installed');
expect(vm.installButtonLoading).toEqual(false);
expect(vm.installButtonDisabled).toEqual(true);
});
it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => { it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => {
vm = mountComponent(ApplicationRow, { vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE, ...DEFAULT_APPLICATION_STATE,
......
...@@ -527,28 +527,28 @@ describe Project do ...@@ -527,28 +527,28 @@ describe Project do
end end
describe "#readme_url" do describe "#readme_url" do
let(:project) { create(:project, :repository, path: "somewhere") }
context 'with a non-existing repository' do context 'with a non-existing repository' do
it 'returns nil' do let(:project) { create(:project) }
allow(project.repository).to receive(:tree).with(:head).and_return(nil)
it 'returns nil' do
expect(project.readme_url).to be_nil expect(project.readme_url).to be_nil
end end
end end
context 'with an existing repository' do context 'with an existing repository' do
context 'when no README exists' do context 'when no README exists' do
it 'returns nil' do let(:project) { create(:project, :empty_repo) }
allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
it 'returns nil' do
expect(project.readme_url).to be_nil expect(project.readme_url).to be_nil
end end
end end
context 'when a README exists' do context 'when a README exists' do
let(:project) { create(:project, :repository) }
it 'returns the README' do it 'returns the README' do
expect(project.readme_url).to eql("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere/blob/master/README.md") expect(project.readme_url).to eq("#{project.web_url}/blob/master/README.md")
end end
end end
end end
......
...@@ -461,6 +461,24 @@ describe Repository do ...@@ -461,6 +461,24 @@ describe Repository do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
context 'regular blob' do
subject { repository.blob_at(repository.head_commit.sha, '.gitignore') }
it { is_expected.to be_an_instance_of(::Blob) }
end
context 'readme blob on HEAD' do
subject { repository.blob_at(repository.head_commit.sha, 'README.md') }
it { is_expected.to be_an_instance_of(::ReadmeBlob) }
end
context 'readme blob not on HEAD' do
subject { repository.blob_at(repository.find_branch('feature').target, 'README.md') }
it { is_expected.to be_an_instance_of(::Blob) }
end
end end
describe '#merged_to_root_ref?' do describe '#merged_to_root_ref?' do
...@@ -1922,23 +1940,25 @@ describe Repository do ...@@ -1922,23 +1940,25 @@ describe Repository do
describe '#readme', :use_clean_rails_memory_store_caching do describe '#readme', :use_clean_rails_memory_store_caching do
context 'with a non-existing repository' do context 'with a non-existing repository' do
it 'returns nil' do let(:project) { create(:project) }
allow(repository).to receive(:tree).with(:head).and_return(nil)
it 'returns nil' do
expect(repository.readme).to be_nil expect(repository.readme).to be_nil
end end
end end
context 'with an existing repository' do context 'with an existing repository' do
context 'when no README exists' do context 'when no README exists' do
it 'returns nil' do let(:project) { create(:project, :empty_repo) }
allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
it 'returns nil' do
expect(repository.readme).to be_nil expect(repository.readme).to be_nil
end end
end end
context 'when a README exists' do context 'when a README exists' do
let(:project) { create(:project, :repository) }
it 'returns the README' do it 'returns the README' do
expect(repository.readme).to be_an_instance_of(ReadmeBlob) expect(repository.readme).to be_an_instance_of(ReadmeBlob)
end end
......
...@@ -457,6 +457,12 @@ describe WikiPage do ...@@ -457,6 +457,12 @@ describe WikiPage do
end end
describe '#historical?' do describe '#historical?' do
let(:page) { wiki.find_page('Update') }
let(:old_version) { page.versions.last.id }
let(:old_page) { wiki.find_page('Update', old_version) }
let(:latest_version) { page.versions.first.id }
let(:latest_page) { wiki.find_page('Update', latest_version) }
before do before do
create_page('Update', 'content') create_page('Update', 'content')
@page = wiki.find_page('Update') @page = wiki.find_page('Update')
...@@ -468,23 +474,27 @@ describe WikiPage do ...@@ -468,23 +474,27 @@ describe WikiPage do
end end
it 'returns true when requesting an old version' do it 'returns true when requesting an old version' do
old_version = @page.versions.last.id expect(old_page.historical?).to be_truthy
old_page = wiki.find_page('Update', old_version)
expect(old_page.historical?).to eq true
end end
it 'returns false when requesting latest version' do it 'returns false when requesting latest version' do
latest_version = @page.versions.first.id expect(latest_page.historical?).to be_falsy
latest_page = wiki.find_page('Update', latest_version)
expect(latest_page.historical?).to eq false
end end
it 'returns false when version is nil' do it 'returns false when version is nil' do
latest_page = wiki.find_page('Update', nil) expect(latest_page.historical?).to be_falsy
end
it 'returns false when the last version is nil' do
expect(old_page).to receive(:last_version) { nil }
expect(old_page.historical?).to be_falsy
end
it 'returns false when the version is nil' do
expect(old_page).to receive(:version) { nil }
expect(latest_page.historical?).to eq false expect(old_page.historical?).to be_falsy
end end
end end
......
module WikiHelpers
extend self
def upload_file_to_wiki(project, user, file_name)
opts = {
file_name: file_name,
file_content: File.read(expand_fixture_path(file_name))
}
::Wikis::CreateAttachmentService.new(project, user, opts)
.execute[:result][:file_path]
end
end
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