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>
/* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from '../constants';
/* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_STATUS,
REQUEST_LOADING,
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from '../constants';
export default {
components: {
loadingButton,
identicon,
export default {
components: {
loadingButton,
identicon,
},
props: {
id: {
type: String,
required: true,
},
props: {
id: {
type: String,
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: () => ({}),
},
title: {
type: String,
required: true,
},
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
);
},
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) ||
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: {
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_SUCCESS) && this.isKnownStatus;
},
installButtonLabel() {
let label;
if (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
this.status === APPLICATION_STATUS.ERROR ||
this.isUnknownStatus
) {
label = s__('ClusterIntegration|Install');
} else if (this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING) {
label = s__('ClusterIntegration|Installing');
} else if (this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED) {
label = s__('ClusterIntegration|Installed');
}
this.requestStatus === REQUEST_SUCCESS) &&
this.isKnownStatus
);
},
installButtonLabel() {
let label;
if (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
this.status === APPLICATION_STATUS.ERROR ||
this.isUnknownStatus
) {
label = s__('ClusterIntegration|Install');
} else if (
this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING
) {
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;
},
showManageButton() {
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
return this.status === APPLICATION_STATUS.ERROR ||
this.requestStatus === REQUEST_FAILURE;
},
generalErrorDescription() {
return sprintf(
s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
},
);
},
return label;
},
showManageButton() {
return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
},
manageButtonLabel() {
return s__('ClusterIntegration|Manage');
},
hasError() {
return this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE;
},
generalErrorDescription() {
return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
title: this.title,
});
},
methods: {
installClicked() {
eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
},
methods: {
installClicked() {
eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
};
},
};
</script>
<template>
......
......@@ -6,6 +6,7 @@ export const APPLICATION_STATUS = {
INSTALLING: 'installing',
INSTALLED: 'installed',
UPDATED: 'updated',
UPDATING: 'updating',
ERROR: 'errored',
};
......
......@@ -340,6 +340,8 @@
}
.note-form-actions {
color: $gl-text-color;
@include media-breakpoint-down(xs) {
.btn {
float: none;
......
......@@ -8,7 +8,7 @@ module SendsBlob
include SendFileUpload
end
def send_blob(blob, params = {})
def send_blob(repository, blob, params = {})
if blob
headers['X-Content-Type-Options'] = 'nosniff'
......
......@@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController
def show
@blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
send_blob(@blob)
send_blob(@repository, @blob)
end
def destroy
......
......@@ -12,6 +12,6 @@ class Projects::RawController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@commit.id, @path)
send_blob(@blob, inline: (params[:inline] != 'false'))
send_blob(@repository, @blob, inline: (params[:inline] != 'false'))
end
end
......@@ -2,6 +2,7 @@
class Projects::WikisController < Projects::ApplicationController
include PreviewMarkdown
include SendsBlob
include Gitlab::Utils::StrongMemoize
before_action :authorize_read_wiki!
......@@ -26,16 +27,8 @@ class Projects::WikisController < Projects::ApplicationController
set_encoding_error unless valid_encoding?
render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
response.headers['Content-Security-Policy'] = "default-src 'none'"
response.headers['X-Content-Security-Policy'] = "default-src 'none'"
send_data(
file.raw_data,
type: file.mime_type,
disposition: 'inline',
filename: file.name
)
elsif file_blob
send_blob(@project_wiki.repository, file_blob)
elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
@page = build_page(title: params[:id])
......@@ -164,4 +157,14 @@ class Projects::WikisController < Projects::ApplicationController
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."
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
......@@ -150,7 +150,9 @@ module BlobHelper
# example of Javascript) we tell the browser of the victim not to
# execute untrusted data.
def safe_content_type(blob)
if blob.text?
if blob.extension == 'svg'
blob.mime_type
elsif blob.text?
'text/plain; charset=utf-8'
elsif blob.image?
blob.content_type
......@@ -159,6 +161,12 @@ module BlobHelper
end
end
def content_disposition(blob, inline)
return 'attachment' if blob.extension == 'svg'
inline ? 'inline' : 'attachment'
end
def ref_project
@ref_project ||= @target_project || @project
end
......
......@@ -6,7 +6,7 @@ module WorkhorseHelper
# Send a Git blob through Workhorse
def send_git_blob(repository, blob, inline: true)
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)
render plain: ""
end
......
......@@ -56,7 +56,6 @@ class PrometheusService < MonitoringService
name: 'api_url',
title: 'API URL',
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
}
]
......
......@@ -487,7 +487,20 @@ class Repository
end
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
nil
end
......@@ -569,9 +582,7 @@ class Repository
cache_method :merge_request_template_names, fallback: []
def readme
if readme = tree(:head)&.readme
ReadmeBlob.new(readme, self)
end
head_tree&.readme
end
def rendered_readme
......
......@@ -2,6 +2,7 @@
class Tree
include Gitlab::MarkupHelper
include Gitlab::Utils::StrongMemoize
attr_accessor :repository, :sha, :path, :entries
......@@ -16,32 +17,36 @@ class Tree
@entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive)
end
def readme
return @readme if defined?(@readme)
available_readmes = blobs.select do |blob|
Gitlab::FileDetector.type_of(blob.name) == :readme
end
previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name)
end
plain_readmes = available_readmes.select do |blob|
plain?(blob.name)
def readme_path
strong_memoize(:readme_path) do
available_readmes = blobs.select do |blob|
Gitlab::FileDetector.type_of(blob.name) == :readme
end
previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name)
end
plain_readmes = available_readmes.select do |blob|
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
# Prioritize previewable over plain readmes
readme_tree = previewable_readmes.first || plain_readmes.first
# Return if we can't preview any of them
if readme_tree.nil?
return @readme = nil
def readme
strong_memoize(:readme) do
repository.blob_at(sha, readme_path) if readme_path
end
readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
@readme = repository.blob_at(sha, readme_path)
end
def trees
......
......@@ -160,7 +160,9 @@ class WikiPage
# Returns boolean True or False if this instance
# is an old version of the page.
def historical?
@page.historical? && last_version.sha != version.sha
return false unless last_commit_sha && version
@page.historical? && last_commit_sha != version.sha
end
# Returns boolean True or False if this instance
......
......@@ -2,4 +2,5 @@
= link_to wiki_page.title, project_wiki_path(@project, wiki_page)
%small (#{wiki_page.format})
.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 @@
.nav-text
%h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
= (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
#{time_ago_with_tooltip(@page.last_version.authored_date)}
- if @page.last_version
= (_("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
= 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
| ~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) |
If an issue seems to fall between two priority labels, assign it to the higher-
priority label.
## Severity labels
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. |
| ~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 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
# and `safe_max_bytes`
#
# :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
# exceeded
def filter_diff_options(options, default_options = {})
......
......@@ -4908,9 +4908,6 @@ msgstr ""
msgid "PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments"
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"
msgstr ""
......
......@@ -36,6 +36,7 @@ module QA
# GitLab QA fabrication mechanisms
#
module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base'
autoload :Dependency, 'qa/factory/dependency'
autoload :Product, 'qa/factory/product'
......
This diff is collapsed.
# 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 'capybara/dsl'
module QA
module Factory
class Base
extend SingleForwardable
include ApiFabricator
extend Capybara::DSL
def_delegators :evaluator, :dependency, :dependencies
def_delegators :evaluator, :product, :attributes
......@@ -12,46 +17,96 @@ module QA
raise NotImplementedError
end
def self.fabricate!(*args)
new.tap do |factory|
yield factory if block_given?
def self.fabricate!(*args, &prepare_block)
fabricate_via_api!(*args, &prepare_block)
rescue NotImplementedError
fabricate_via_browser_ui!(*args, &prepare_block)
end
dependencies.each do |name, signature|
Factory::Dependency.new(name, factory, signature).build!
end
def self.fabricate_via_browser_ui!(*args, &prepare_block)
options = args.extract_options!
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
private_class_method :log_fabrication
def self.evaluator
@evaluator ||= Factory::Base::DSL.new(self)
end
private_class_method :evaluator
class DSL
attr_reader :dependencies, :attributes
def initialize(base)
@base = base
@dependencies = {}
@attributes = {}
@dependencies = []
@attributes = []
end
def dependency(factory, as:, &block)
as.tap do |name|
@base.class_eval { attr_accessor name }
Dependency::Signature.new(factory, block).tap do |signature|
@dependencies.store(name, signature)
Dependency::Signature.new(name, factory, block).tap do |signature|
@dependencies << signature
end
end
end
def product(attribute, &block)
Product::Attribute.new(attribute, block).tap do |signature|
@attributes.store(attribute, signature)
@attributes << signature
end
end
end
......
module QA
module Factory
class Dependency
Signature = Struct.new(:factory, :block)
Signature = Struct.new(:name, :factory, :block)
def initialize(name, factory, signature)
@name = name
@factory = factory
@signature = signature
def initialize(caller_factory, dependency_signature)
@caller_factory = caller_factory
@dependency_signature = dependency_signature
end
def overridden?
!!@factory.public_send(@name)
!!@caller_factory.public_send(@dependency_signature.name)
end
def build!
def build!(parents: [])
return if overridden?
Builder.new(@signature, @factory).fabricate!.tap do |product|
@factory.public_send("#{@name}=", product)
end
end
class Builder
def initialize(signature, caller_factory)
@factory = signature.factory
@block = signature.block
@caller_factory = caller_factory
dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
@dependency_signature.block&.call(factory, @caller_factory)
end
def fabricate!
@factory.fabricate! do |factory|
@block&.call(factory, @caller_factory)
end
dependency.tap do |dependency|
@caller_factory.public_send("#{@dependency_signature.name}=", dependency)
end
end
end
......
......@@ -5,26 +5,46 @@ module QA
class Product
include Capybara::DSL
NoValueError = Class.new(RuntimeError)
attr_reader :factory, :web_url
Attribute = Struct.new(:name, :block)
def initialize
@location = current_url
def initialize(factory, web_url)
@factory = factory
@web_url = web_url
populate_attributes!
end
def visit!
visit @location
visit(web_url)
end
def self.populate!(factory, web_url)
new(factory, web_url)
end
def self.populate!(factory)
new.tap do |product|
factory.class.attributes.each_value do |attribute|
product.instance_exec(factory, attribute.block) do |factory, block|
value = block.call(factory)
product.define_singleton_method(attribute.name) { value }
end
private
def populate_attributes!
factory.class.attributes.each do |attribute|
instance_exec(factory, attribute.block) do |factory, block|
value = attribute_value(attribute, block)
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
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
......@@ -7,13 +7,8 @@ module QA
project.description = 'Project with repository'
end
product :output do |factory|
factory.output
end
product :project do |factory|
factory.project
end
product :output
product :project
def initialize
@file_name = 'file.txt'
......
......@@ -11,7 +11,7 @@ module QA
end
end
product(:user) { |factory| factory.user }
product :user
def visit_project_with_retry
# The user intermittently fails to stay signed in after visiting the
......
......@@ -6,6 +6,10 @@ module QA
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
@path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}"
......@@ -35,6 +39,29 @@ module QA
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
......
......@@ -2,16 +2,15 @@ module QA
module Factory
module Resource
class Issue < Factory::Base
attr_writer :title, :description, :project
attr_accessor :title, :description, :project
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
end
product :title do
Page::Project::Issue::Show.act { issue_title }
end
product :project
product :title
def fabricate!
project.visit!
......
......@@ -12,13 +12,8 @@ module QA
:milestone,
:labels
product :project do |factory|
factory.project
end
product :source_branch do |factory|
factory.source_branch
end
product :project
product :source_branch
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-merge-request'
......
......@@ -4,14 +4,13 @@ module QA
module Factory
module Resource
class Project < Factory::Base
attr_writer :description
attr_accessor :description
attr_reader :name
dependency Factory::Resource::Group, as: :group
product :name do |factory|
factory.name
end
product :group
product :name
product :repository_ssh_location do
Page::Project::Show.act do
......@@ -48,6 +47,32 @@ module QA
page.create_new_project
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
......
......@@ -8,9 +8,7 @@ module QA
dependency Factory::Resource::Group, as: :group
product :name do |factory|
factory.name
end
product :name
def fabricate!
group.visit!
......
......@@ -7,7 +7,7 @@ module QA
dependency Factory::Resource::Project, as: :project
product(:title) { |factory| factory.title }
product :title
def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}"
......
......@@ -6,21 +6,28 @@ module QA
# creating it if it doesn't yet exist.
#
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
@name = Runtime::Namespace.sandbox_name
@path = Runtime::Namespace.sandbox_name
end
def fabricate!
Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
if page.has_group?(@name)
page.go_to_group(@name)
if page.has_group?(path)
page.go_to_group(path)
else
page.go_to_new_group
Page::Group::New.perform do |group|
group.set_path(@name)
group.set_path(path)
group.set_description('GitLab QA Sandbox Group')
group.set_visibility('Public')
group.create
......@@ -28,6 +35,28 @@ module QA
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
......
......@@ -10,17 +10,9 @@ module QA
attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint
product :private_key do |factory|
factory.private_key
end
product :title do |factory|
factory.title
end
product :fingerprint do |factory|
factory.fingerprint
end
product :private_key
product :title
product :fingerprint
def key
@key ||= Runtime::Key::RSA.new
......
......@@ -31,10 +31,10 @@ module QA
defined?(@username) && defined?(@password)
end
product(:name) { |factory| factory.name }
product(:username) { |factory| factory.username }
product(:email) { |factory| factory.email }
product(:password) { |factory| factory.password }
product :name
product :username
product :email
product :password
def fabricate!
# Don't try to log-out if we're not logged-in
......
......@@ -10,13 +10,16 @@ module QA
end
def fabricate!
Page::Project::Menu.act { click_wiki }
Page::Project::Wiki::New.perform do |page|
page.go_to_create_first_page
page.set_title(@title)
page.set_content(@content)
page.set_message(@message)
page.create_new_page
project.visit!
Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
Page::Project::Wiki::New.perform do |wiki_new|
wiki_new.go_to_create_first_page
wiki_new.set_title(@title)
wiki_new.set_content(@content)
wiki_new.set_message(@message)
wiki_new.create_new_page
end
end
end
......
......@@ -131,4 +131,4 @@ 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 QA issue tracker.
open an issue in GitLab CE issue tracker with the `~QA` label.
......@@ -6,33 +6,34 @@ module QA
class Client
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
@personal_access_token = personal_access_token
@is_new_session = is_new_session
end
def personal_access_token
@personal_access_token ||= get_personal_access_token
end
def get_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
@personal_access_token ||= begin
# you can set the environment variable PERSONAL_ACCESS_TOKEN
# to use a specific access token rather than create one from the UI
Runtime::Env.personal_access_token ||= create_personal_access_token
end
end
private
def create_personal_access_token
Runtime::Browser.visit(@address, Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::PersonalAccessToken.fabricate!.access_token
if @is_new_session
Runtime::Browser.visit(@address, Page::Main::Login) { do_create_personal_access_token }
else
do_create_personal_access_token
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
......
......@@ -3,6 +3,12 @@ module QA
module Env
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
def chrome_headless?
enabled?(ENV['CHROME_HEADLESS'])
......@@ -22,7 +28,7 @@ module QA
# specifies token that can be used for the api
def personal_access_token
ENV['PERSONAL_ACCESS_TOKEN']
@personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN']
end
def user_username
......@@ -42,7 +48,7 @@ module QA
end
def forker?
forker_username && forker_password
!!(forker_username && forker_password)
end
def forker_username
......
......@@ -11,9 +11,10 @@ module QA
Page::Main::Menu.perform { |main| main.sign_out }
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'
end
project.visit!
Page::Project::Menu.act { click_members_settings }
Page::Project::Settings::Members.perform do |page|
......
......@@ -7,17 +7,15 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
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.description = 'create awesome project test'
end
expect(created_project.name).to match /^awesome-project-\h{16}$/
expect(page).to have_content(created_project.name)
expect(page).to have_content(
/Project \S?awesome-project\S+ was successfully created/
)
expect(page).to have_content('create awesome project test')
expect(page).to have_content('The repository for this project is empty')
end
......
......@@ -10,6 +10,7 @@ module QA
project = Factory::Resource::Project.fabricate! do |project|
project.name = "only-fast-forward"
end
project.visit!
Page::Project::Menu.act { go_to_settings }
Page::Project::Settings::MergeRequest.act { enable_ff_only }
......
......@@ -14,10 +14,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
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.description = 'project for git clone tests'
end
project.visit!
Git::Repository.perform do |repository|
repository.uri = location.uri
......
......@@ -17,6 +17,7 @@ module QA
project.name = 'file-template-project'
project.description = 'Add file templates via the Web IDE'
end
@project.visit!
# Add a file via the regular Files view because the Web IDE isn't
# 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
include Support::StubENV
let(:factory) { spy('factory') }
let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
describe '.fabricate!' do
subject { Class.new(described_class) }
shared_context 'fabrication context' do
subject do
Class.new(described_class) do
def self.name
'MyFactory'
end
end
end
before do
allow(QA::Factory::Product).to receive(:new).and_return(product)
allow(QA::Factory::Product).to receive(:populate!).and_return(product)
allow(subject).to receive(:current_url).and_return(product_location)
allow(subject).to receive(:new).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).with(factory, product_location).and_return(product)
end
end
it 'instantiates the factory and calls factory method' do
expect(subject).to receive(:new).and_return(factory)
shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
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
it 'returns fabrication product' do
allow(subject).to receive(:new).and_return(factory)
it 'does not log the factory and build method when VERBOSE=false' do
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
it 'yields factory before calling factory method' do
allow(subject).to receive(:new).and_return(factory)
context 'when factory supports fabrication via the API' do
it 'calls .fabricate_via_browser_ui!' do
expect(described_class).to receive(:fabricate_via_api!)
subject.fabricate! do |factory|
factory.something!
described_class.fabricate!
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
expect(factory).to have_received(:fabricate!).ordered
result = subject.fabricate_via_api!(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(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
......@@ -75,9 +152,9 @@ describe QA::Factory::Base do
stub_const('Some::MyDependency', dependency)
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(QA::Factory::Product).to receive(:new)
allow(QA::Factory::Product).to receive(:populate!)
expect(QA::Factory::Product).to receive(:populate!)
end
it 'builds all dependencies first' do
......@@ -89,44 +166,22 @@ describe QA::Factory::Base do
end
describe '.product' do
include_context 'fabrication context'
subject do
Class.new(described_class) do
def fabricate!
"any"
end
# Defined only to be stubbed
def self.find_page
end
product :token do
find_page.do_something_on_page!
'resulting value'
end
product :token
end
end
it 'appends new product attribute' do
expect(subject.attributes).to be_one
expect(subject.attributes).to have_key(:token)
end
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
expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
expect(subject.attributes[0].name).to eq(:token)
end
end
end
......@@ -4,11 +4,11 @@ describe QA::Factory::Dependency do
let(:block) { spy('block') }
let(:signature) do
double('signature', factory: dependency, block: block)
double('signature', name: :mydep, factory: dependency, block: block)
end
subject do
described_class.new(:mydep, factory, signature)
described_class.new(factory, signature)
end
describe '#overridden?' do
......@@ -55,16 +55,23 @@ describe QA::Factory::Dependency do
expect(factory).to have_received(:mydep=).with(dependency)
end
context 'when receives a caller factory as block argument' do
let(:dependency) { QA::Factory::Base }
it 'calls given block with dependency factory and caller factory' do
expect(dependency).to receive(:fabricate!).and_yield(dependency)
it 'calls given block with dependency factory and caller factory' do
allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any'))
subject.build!
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!
expect(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory)
expect(dependency).to have_received(:fabricate!)
end
end
end
......
describe QA::Factory::Product do
let(:factory) do
QA::Factory::Base.new
end
let(:attributes) do
{ test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) }
Class.new(QA::Factory::Base) do
def foo
'bar'
end
end.new
end
let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
before do
allow(QA::Factory::Base).to receive(:attributes).and_return(attributes)
end
subject { described_class.new(factory, product_location) }
describe '.populate!' do
it 'returns a fabrication product and define factory attributes as its methods' do
expect(described_class).to receive(:new).and_return(product)
before do
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|
instance.something = 'string'
it 'returns a fabrication product and defines factory attributes as its methods' do
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
expect(result).to be product
expect(result.test).to eq('returned')
context 'when the product attribute is populated via a factory attribute' do
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
describe '.visit!' 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)
.to receive(:visit).and_return('visited some url')
......
......@@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do
end
end
describe '#get_personal_access_token' do
it 'returns specified token from env' do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
describe '#personal_access_token' do
context 'when QA::Runtime::Env.personal_access_token is present' do
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
it 'returns a created token' do
allow_any_instance_of(described_class)
.to receive(:create_personal_access_token).and_return('created_token')
context 'when QA::Runtime::Env.personal_access_token is nil' do
before do
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
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
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
allow(client).to receive(:personal_access_token).and_return('a_token')
end
let(:client) { QA::Runtime::API::Client.new('http://example.com') }
let(:request) { described_class.new(client, '/users') }
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'
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
describe '#request_path' do
......
......@@ -34,6 +34,10 @@ describe QA::Runtime::Env do
end
end
describe '.verbose?' do
it_behaves_like 'boolean method', :verbose?, 'VERBOSE', false
end
describe '.signup_disabled?' do
it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false
end
......@@ -64,7 +68,54 @@ describe QA::Runtime::Env do
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
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
expect(described_class).not_to be_forker
end
......
......@@ -22,20 +22,18 @@ describe Projects::WikisController do
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
expect(controller).to receive(:load_wiki).and_return(project_wiki)
it 'limits the retrieved pages for the sidebar' do
expect(controller).to receive(:load_wiki).and_return(project_wiki)
# empty? call
expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original
# Sidebar entries
expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original
# empty? call
expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original
# Sidebar entries
expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original
subject
subject
expect(response).to have_http_status(:ok)
expect(response.body).to include(wiki_title)
end
expect(response).to have_http_status(:ok)
expect(response.body).to include(wiki_title)
end
context 'when page content encoding is invalid' 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.'
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
describe 'POST #preview_markdown' do
......
......@@ -54,7 +54,7 @@ describe 'User views empty wiki' do
it_behaves_like 'empty wiki and non-accessible issues'
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) }
before do
......
......@@ -2,12 +2,15 @@ require 'spec_helper'
describe 'User views a wiki page' do
shared_examples 'wiki page user view' do
include WikiHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:path) { 'image.png' }
let(:wiki_page) do
create(:wiki_page,
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
before do
......@@ -82,33 +85,26 @@ describe 'User views a wiki page' do
expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
end
it 'shows a file stored in a page' do
raw_file = Gitlab::GitalyClient::WikiFile.new(
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)
context 'shows a file stored in a page' do
let(:path) { upload_file_to_wiki(project, user, 'dk.png') }
expect(page).to have_xpath("//img[@data-src='#{project.wiki.wiki_base_path}/image.jpg']")
expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg")
it do
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(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
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
end
end
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')
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('Create page')
end
......
This diff is collapsed.
......@@ -112,6 +112,17 @@ describe('Application Row', () => {
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', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
......
......@@ -527,28 +527,28 @@ describe Project do
end
describe "#readme_url" do
let(:project) { create(:project, :repository, path: "somewhere") }
context 'with a non-existing repository' do
it 'returns nil' do
allow(project.repository).to receive(:tree).with(:head).and_return(nil)
let(:project) { create(:project) }
it 'returns nil' do
expect(project.readme_url).to be_nil
end
end
context 'with an existing repository' do
context 'when no README exists' do
it 'returns nil' do
allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
let(:project) { create(:project, :empty_repo) }
it 'returns nil' do
expect(project.readme_url).to be_nil
end
end
context 'when a README exists' do
let(:project) { create(:project, :repository) }
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
......
......@@ -461,6 +461,24 @@ describe Repository do
it { is_expected.to be_nil }
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
describe '#merged_to_root_ref?' do
......@@ -1922,23 +1940,25 @@ describe Repository do
describe '#readme', :use_clean_rails_memory_store_caching do
context 'with a non-existing repository' do
it 'returns nil' do
allow(repository).to receive(:tree).with(:head).and_return(nil)
let(:project) { create(:project) }
it 'returns nil' do
expect(repository.readme).to be_nil
end
end
context 'with an existing repository' do
context 'when no README exists' do
it 'returns nil' do
allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
let(:project) { create(:project, :empty_repo) }
it 'returns nil' do
expect(repository.readme).to be_nil
end
end
context 'when a README exists' do
let(:project) { create(:project, :repository) }
it 'returns the README' do
expect(repository.readme).to be_an_instance_of(ReadmeBlob)
end
......
......@@ -457,6 +457,12 @@ describe WikiPage do
end
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
create_page('Update', 'content')
@page = wiki.find_page('Update')
......@@ -468,23 +474,27 @@ describe WikiPage do
end
it 'returns true when requesting an old version' do
old_version = @page.versions.last.id
old_page = wiki.find_page('Update', old_version)
expect(old_page.historical?).to eq true
expect(old_page.historical?).to be_truthy
end
it 'returns false when requesting latest version' do
latest_version = @page.versions.first.id
latest_page = wiki.find_page('Update', latest_version)
expect(latest_page.historical?).to eq false
expect(latest_page.historical?).to be_falsy
end
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
......
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