提交 abe11a6a 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 95e18e32
......@@ -455,7 +455,7 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 12.9.0.pre.rc4'
gem 'gitaly', '~> 13.0.0.pre.rc1'
gem 'grpc', '~> 1.24.0'
......
......@@ -378,7 +378,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
git (1.5.0)
gitaly (12.9.0.pre.rc4)
gitaly (13.0.0.pre.rc1)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-chronic (0.10.5)
......@@ -1236,7 +1236,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 12.9.0.pre.rc4)
gitaly (~> 13.0.0.pre.rc1)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-labkit (= 0.12.0)
......
import Vue from 'vue';
import canaryCalloutMixin from 'ee_else_ce/environments/mixins/canary_callout_mixin';
import canaryCalloutMixin from '../mixins/canary_callout_mixin';
import environmentsFolderApp from './environments_folder_view.vue';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
......
<script>
import folderMixin from 'ee_else_ce/environments/mixins/environments_folder_view_mixin';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
......@@ -11,7 +10,7 @@ export default {
DeleteEnvironmentModal,
},
mixins: [environmentsMixin, CIPaginationMixin, folderMixin],
mixins: [environmentsMixin, CIPaginationMixin],
props: {
endpoint: {
......@@ -30,6 +29,31 @@ export default {
type: Boolean,
required: true,
},
canaryDeploymentFeatureId: {
type: String,
required: false,
default: '',
},
showCanaryDeploymentCallout: {
type: Boolean,
required: false,
default: false,
},
userCalloutsPath: {
type: String,
required: false,
default: '',
},
lockPromotionSvgPath: {
type: String,
required: false,
default: '',
},
helpCanaryDeploymentsPath: {
type: String,
required: false,
default: '',
},
},
methods: {
successCallback(resp) {
......
import Vue from 'vue';
import canaryCalloutMixin from 'ee_else_ce/environments/mixins/canary_callout_mixin';
import canaryCalloutMixin from './mixins/canary_callout_mixin';
import environmentsComponent from './components/environments_app.vue';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
......
import { parseBoolean } from '~/lib/utils/common_utils';
export default {
data() {
const data = document.querySelector(this.$options.el).dataset;
return {
canaryDeploymentFeatureId: data.environmentsDataCanaryDeploymentFeatureId,
showCanaryDeploymentCallout: parseBoolean(data.environmentsDataShowCanaryDeploymentCallout),
userCalloutsPath: data.environmentsDataUserCalloutsPath,
lockPromotionSvgPath: data.environmentsDataLockPromotionSvgPath,
helpCanaryDeploymentsPath: data.environmentsDataHelpCanaryDeploymentsPath,
};
},
computed: {
canaryCalloutProps() {},
canaryCalloutProps() {
return {
canaryDeploymentFeatureId: this.canaryDeploymentFeatureId,
showCanaryDeploymentCallout: this.showCanaryDeploymentCallout,
userCalloutsPath: this.userCalloutsPath,
lockPromotionSvgPath: this.lockPromotionSvgPath,
helpCanaryDeploymentsPath: this.helpCanaryDeploymentsPath,
};
},
},
};
export default {
props: {
canaryDeploymentFeatureId: {
type: String,
required: false,
default: '',
},
showCanaryDeploymentCallout: {
type: Boolean,
required: false,
default: false,
},
userCalloutsPath: {
type: String,
required: false,
default: '',
},
lockPromotionSvgPath: {
type: String,
required: false,
default: '',
},
helpCanaryDeploymentsPath: {
type: String,
required: false,
default: '',
},
},
};
......@@ -15,6 +15,8 @@ module Mutations
end
def authorized_resource?(snippet)
return false if snippet.nil?
Ability.allowed?(context[:current_user], ability_for(snippet), snippet)
end
......
......@@ -106,7 +106,23 @@ class RemoteMirror < ApplicationRecord
update_status == 'started'
end
def update_repository(options)
def update_repository
Gitlab::Git::RemoteMirror.new(
project.repository.raw,
remote_name,
**options_for_update
).update
end
def options_for_update
options = {
keep_divergent_refs: keep_divergent_refs?
}
if only_protected_branches?
options[:only_branches_matching] = project.protected_branches.pluck(:name)
end
if ssh_mirror_url?
if ssh_key_auth? && ssh_private_key.present?
options[:ssh_key] = ssh_private_key
......@@ -117,13 +133,7 @@ class RemoteMirror < ApplicationRecord
end
end
options[:keep_divergent_refs] = keep_divergent_refs?
Gitlab::Git::RemoteMirror.new(
project.repository.raw,
remote_name,
**options
).update
options
end
def sync?
......
......@@ -29,14 +29,16 @@ module Projects
remote_mirror.ensure_remote!
repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true)
opts = {}
if remote_mirror.only_protected_branches?
opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name)
end
response = remote_mirror.update_repository
remote_mirror.update_repository(opts)
if response.divergent_refs.any?
message = "Some refs have diverged and have not been updated on the remote:"
message += "\n\n#{response.divergent_refs.join("\n")}"
remote_mirror.update_finish!
remote_mirror.mark_as_failed!(message)
else
remote_mirror.update_finish!
end
end
def retry_or_fail(mirror, message, tries)
......
---
title: Add snippet repository backfilling migration
merge_request: 29927
author:
type: other
---
title: Fix 500 error for non-existing snippet on graphql mutations
merge_request: 30632
author: Sashi Kumar
type: fixed
# frozen_string_literal: true
class BackfillSnippetRepositories < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INTERVAL = 3.minutes
BATCH_SIZE = 100
MIGRATION = 'BackfillSnippetRepositories'
disable_ddl_transaction!
class Snippet < ActiveRecord::Base
include EachBatch
self.table_name = 'snippets'
self.inheritance_column = :_type_disabled
end
def up
queue_background_migration_jobs_by_range_at_intervals(Snippet,
MIGRATION,
INTERVAL,
batch_size: BATCH_SIZE)
end
def down
# no-op
end
end
......@@ -13545,6 +13545,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200417044453
20200417145946
20200420092011
20200420094444
20200420104303
20200420104323
20200420162730
......
......@@ -138,6 +138,7 @@ initializer
initializers
interdependencies
interdependency
interruptible
Irker
Istio
jasmine-jquery
......@@ -294,6 +295,7 @@ Splunk
SSH
storable
strace
subpath
subfolder
subfolders
sublicense
......@@ -309,6 +311,8 @@ subqueried
subqueries
subquery
subquerying
substring
substrings
syslog
Tiller
todos
......@@ -363,6 +367,7 @@ unreferenced
unresolve
unresolved
unresolving
unschedule
unstage
unstaged
unstages
......
此差异已折叠。
......@@ -77,13 +77,24 @@ whether the move is necessary), and ensure that a technical writer reviews this
change prior to merging.
If you indeed need to change a document's location, do not remove the old
document, but instead replace all of its content with a new line:
document, but instead replace all of its content with the following:
```md
This document was moved to [another location](path/to/new_doc.md).
---
redirect_to: '../path/to/file/index.md'
---
This document was moved to [another location](../path/to/file/index.md).
```
where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
Where `../path/to/file/index.md` is usually the relative path to the old document.
The `redirect_to` variable supports both full and relative URLs, for example
`https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`.
It ensures that the redirect will work for <https://docs.gitlab.com> and any `*.md` paths
will be compiled to `*.html`.
The new line underneath the frontmatter informs the user that the document
changed location and is useful for someone that browses that file from the repository.
For example, if you move `doc/workflow/lfs/index.md` to
`doc/administration/lfs.md`, then the steps would be:
......@@ -92,12 +103,16 @@ For example, if you move `doc/workflow/lfs/index.md` to
1. Replace the contents of `doc/workflow/lfs/index.md` with:
```md
---
redirect_to: '../../administration/lfs.md'
---
This document was moved to [another location](../../administration/lfs.md).
```
1. Find and replace any occurrences of the old location with the new one.
A quick way to find them is to use `git grep`. First go to the root directory
where you cloned the `gitlab` repository and then do:
A quick way to find them is to use `git grep` on the repository you changed
the file from:
```shell
git grep -n "workflow/lfs/lfs_administration"
......@@ -124,24 +139,6 @@ Things to note:
built-in help page, that's why we omit it in `git grep`.
- Use the checklist on the "Change documentation location" MR description template.
### Alternative redirection method
You can also replace the content
of the old file with a frontmatter containing a redirect link:
```yaml
---
redirect_to: '../path/to/file/README.md'
---
```
It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`.
NOTE: **Note:**
This redirection method will not provide a redirect fallback on GitLab `/help`. When using
it, make sure to add a link to the new page on the doc, otherwise it's a dead end for users that
land on the doc via `/help`.
### Redirections for pages with Disqus comments
If the documentation page being relocated already has Disqus comments,
......
......@@ -2,14 +2,23 @@
Integrating a security scanner into GitLab consists of providing end users
with a [CI job definition](../../ci/yaml/README.md#introduction)
they can add to their CI configuration files, to scan their GitLab projects.
they can add to their CI configuration files to scan their GitLab projects.
This CI job should then output its results in a GitLab-specified format. These results are then
automatically presented in various places in GitLab, such as the Pipeline view, Merge Request
widget, and Security Dashboard.
The scanning job is usually based on a [Docker image](https://docs.docker.com/)
that contains the scanner and all its dependencies in a self-contained environment.
This page documents requirements and guidelines for writing CI jobs implementing a security scanner,
as well as requirements and guidelines for the Docker image itself.
This page documents requirements and guidelines for writing CI jobs that implement a security
scanner, as well as requirements and guidelines for the Docker image.
## Job definition
This section desribes several important fields to add to the security scanner's job
definition file. Full documentation on these and other available fields can be viewed
in the [CI documentation](../../ci/yaml/README.md#image).
### Name
For consistency, scanning jobs should be named after the scanner, in lower case.
......@@ -26,8 +35,8 @@ containing the security scanner.
### Script
The [`script`](../../ci/yaml/README.md#script) keyword
is used to specify the command that the job runs.
Because the `script` cannot be left empty, it must be set to the command that performs the scan.
is used to specify the commands to run the scanner.
Because the `script` entry can't be left empty, it must be set to the command that performs the scan.
It is not possible to rely on the predefined `ENTRYPOINT` and `CMD` of the Docker image
to perform the scan automatically, without passing any command.
......@@ -60,37 +69,34 @@ For example, here is the definition of a SAST job that generates a file named `g
and uploads it as a SAST report:
```yaml
mysec_dependency_scanning:
mysec_sast_scanning:
image: registry.gitlab.com/secure/mysec
artifacts:
reports:
sast: gl-sast-report.json
```
`gl-sast-report.json` is an example file path. See [the Output file section](#output-file) for more details.
It is processed as a SAST report because it is declared as such in the job definition.
Note that `gl-sast-report.json` is an example file path but any other file name can be used. See
[the Output file section](#output-file) for more details. It's processed as a SAST report because
it's declared under the `reports:sast` key in the job definition, not because of the file name.
### Policies
Scanning jobs should be skipped unless the corresponding feature is listed
in the `GITLAB_FEATURES` variable (comma-separated list of values).
So Dependency Scanning, Container Scanning, SAST, and DAST should be skipped
unless `GITLAB_FEATURES` contains `dependency_scanning`, `container_scanning`, `sast`, and `dast`, respectively.
See [GitLab CI/CD predefined variables](../../ci/variables/predefined_variables.md).
Certain GitLab workflows, such as [AutoDevOps](../../topics/autodevops/customize.md#disable-jobs),
define variables to indicate that given scans should be disabled. You can check for this by looking
for variables such as `DEPENDENCY_SCANNING_DISABLED`, `CONTAINER_SCANNING_DISABLED`,
`SAST_DISABLED`, and `DAST_DISABLED`. If appropriate based on the scanner type, you should then
disable running the custom scanner.
Also, scanning jobs should be skipped when the corresponding variable prefixed with `_DISABLED` is present.
See `DEPENDENCY_SCANNING_DISABLED`, `CONTAINER_SCANNING_DISABLED`, `SAST_DISABLED`, and `DAST_DISABLED`
in [Auto DevOps documentation](../../topics/autodevops/customize.md#disable-jobs).
Finally, SAST and Dependency Scanning job definitions should use
`CI_PROJECT_REPOSITORY_LANGUAGES` (comma-separated list of values)
in order to skip the job when the language or technology is not supported.
GitLab also defines a `CI_PROJECT_REPOSITORY_LANGUAGES` variable, which provides the list of
languages in the repo. Depending on this value, your scanner may or may not do something different.
Language detection currently relies on the [`linguist`](https://github.com/github/linguist) Ruby gem.
See [GitLab CI/CD prefined variables](../../ci/variables/predefined_variables.md#variables-reference).
For instance, here is how to skip the Dependency Scanning job `mysec_dependency_scanning`
unless the project repository contains Java source code,
and the `dependency_scanning` feature is enabled:
#### Policy checking example
This example shows how to skip a custom Dependency Scanning job, `mysec_dependency_scanning`, unless
the project repository contains Java source code and the `dependency_scanning` feature is enabled:
```yaml
mysec_dependency_scanning:
......@@ -111,6 +117,8 @@ for a particular branch or when a particular set of files changes.
The Docker image is a self-contained environment that combines
the scanner with all the libraries and tools it depends on.
Packaging your scanner into a Docker image makes its dependencies and configuration always present,
regardless of the individual machine the scanner runs on.
### Image size
......@@ -144,7 +152,7 @@ It also generates text output on the standard output and standard error streams,
All CI variables are passed to the scanner as environment variables.
The scanned project is described by the [predefined CI variables](../../ci/variables/README.md).
#### SAST, Dependency Scanning
#### SAST and Dependency Scanning
SAST and Dependency Scanning scanners must scan the files in the project directory, given by the `CI_PROJECT_DIR` variable.
......@@ -223,11 +231,8 @@ The DAST variant of the report JSON format is not documented at the moment.
### Version
The documentation of
[SAST](../../user/application_security/sast/index.md#reports-json-format),
[Dependency Scanning](../../user/application_security/dependency_scanning/index.md#reports-json-format),
and [Container Scanning](../../user/application_security/container_scanning/index.md#reports-json-format)
describes the Secure report format version.
This field specifies the version of the report schema you are using. Please reference individual scanner
pages for the specific versions to use.
### Vulnerabilities
......@@ -251,12 +256,17 @@ The `id` should not collide with any other scanner another integrator would prov
#### Name, message, and description
The `name` and `message` fields contain a short description of the vulnerability,
whereas the `description` field provides more details.
The `name` and `message` fields contain a short description of the vulnerability.
The `description` field provides more details.
The `name` is context-free and contains no information on where the vulnerability has been found,
The `name` field is context-free and contains no information on where the vulnerability has been found,
whereas the `message` may repeat the location.
As a visual example, this screenshot highlights where these fields are used when viewing a
vulnerability as part of a pipeline view.
![Example Vulnerability](example_vuln.png)
For instance, a `message` for a vulnerability
reported by Dependency Scanning gives information on the vulnerable dependency,
which is redundant with the `location` field of the vulnerability.
......@@ -288,21 +298,17 @@ It should not repeat the other fields of the vulnerability object.
In particular, the `description` should not repeat the `location` (what is affected)
or the `solution` (how to mitigate the risk).
There is a proposal to remove either the `name` or the `message`, to remove ambiguities.
See [issue #36779](https://gitlab.com/gitlab-org/gitlab/issues/36779).
#### Solution
The `solution` field may contain instructions users should follow to fix the vulnerability or to mitigate the risk.
It is intended for users whereas the `remediations` objects are processed automatically by GitLab.
You can use the `solution` field to instruct users how to fix the identified vulnerability or to mitigate
the risk. End-users interact with this field, whereas GitLab automatically processes the
`remediations` objects.
#### Identifiers
The `identifiers` array describes the vulnerability flaw that has been detected.
An identifier object has a `type` and a `value`;
these technical fields are used to tell if two identifiers are the same.
It also has a `name` and a `url`;
these fields are used to display the identifier in the user interface.
The `identifiers` array describes the detected vulnerability. An identifier object's `type` and
`value` fields are used to tell if two identifiers are the same. The user interface uses the
object's `name` and `url` fields to display the identifier.
It is recommended to reuse the identifiers the GitLab scanners already define:
......@@ -316,18 +322,15 @@ It is recommended to reuse the identifiers the GitLab scanners already define:
| [RHSA](https://access.redhat.com/errata/#/) | `rhsa` | RHSA-2020:0111 |
| [ELSA](https://linux.oracle.com/security/) | `elsa` | ELSA-2020-0085 |
The generic identifiers listed above are defined in the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common);
this library is shared by the analyzers maintained by GitLab,
and this is where you can [contribute](https://gitlab.com/gitlab-org/security-products/analyzers/common/blob/master/issue/identifier.go) new generic identifiers.
Analyzers may also produce vendor-specific or product-specific identifiers;
these do not belong to the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common).
The generic identifiers listed above are defined in the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common),
which is shared by the analyzers that GitLab maintains. You can [contribute](https://gitlab.com/gitlab-org/security-products/analyzers/common/blob/master/issue/identifier.go)
new generic identifiers to if needed. Analyzers may also produce vendor-specific or product-specific
identifiers, which don't belong in the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common).
The first item of the `identifiers` array is called the primary identifier.
The primary identifier is particularly important, because it is used to
[track vulnerabilities](#tracking-merging-vulnerabilities)
as new commits are pushed to the repository.
Identifiers are used to [merge duplicate vulnerabilities](#tracking-merging-vulnerabilities)
[track vulnerabilities](#tracking-and-merging-vulnerabilities) as new commits are pushed to the repository.
Identifiers are also used to [merge duplicate vulnerabilities](#tracking-and-merging-vulnerabilities)
reported for the same commit, except for `CWE` and `WASC`.
### Location
......@@ -336,7 +339,7 @@ The `location` indicates where the vulnerability has been detected.
The format of the location depends on the type of scanning.
Internally GitLab extracts some attributes of the `location` to generate the **location fingerprint**,
which is used to [track vulnerabilities](#tracking-merging-vulnerabilities)
which is used to track vulnerabilities
as new commits are pushed to the repository.
The attributes used to generate the location fingerprint also depend on the type of scanning.
......@@ -426,12 +429,12 @@ combines `file`, `start_line`, and `end_line`,
so these attributes are mandatory.
All other attributes are optional.
### Tracking, merging vulnerabilities
### Tracking and merging vulnerabilities
Users may give feedback on a vulnerability:
- they may dismiss a vulnerability if it does not apply to their projects
- or they may create an issue for a vulnerability, if there is a possible threat
- They may dismiss a vulnerability if it doesn't apply to their projects
- They may create an issue for a vulnerability if there's a possible threat
GitLab tracks vulnerabilities so that user feedback is not lost
when new Git commits are pushed to the repository.
......
......@@ -44,16 +44,19 @@ module Gitlab
create_commit(snippet)
end
# Removing the db record
def destroy_snippet_repository(snippet)
# Removing the db record
snippet.snippet_repository&.destroy
snippet.snippet_repository&.delete
rescue => e
logger.error(message: "Snippet Migration: error destroying snippet repository. Reason: #{e.message}", snippet: snippet.id)
end
# Removing the repository in disk
def delete_repository(snippet)
# Removing the repository in disk
snippet.repository.remove if snippet.repository_exists?
return unless snippet.repository_exists?
snippet.repository.remove
snippet.repository.expire_exists_cache
rescue => e
logger.error(message: "Snippet Migration: error deleting repository. Reason: #{e.message}", snippet: snippet.id)
end
......@@ -82,7 +85,24 @@ module Gitlab
end
def create_commit(snippet)
snippet.snippet_repository.multi_files_action(snippet.author, snippet_action(snippet), commit_attrs)
snippet.snippet_repository.multi_files_action(commit_author(snippet), snippet_action(snippet), commit_attrs)
end
# If the user is not allowed to access git or update the snippet
# because it is blocked, internal, ghost, ... we cannot commit
# files because these users are not allowed to, but we need to
# migrate their snippets as well.
# In this scenario an admin user will be the one that will commit the files.
def commit_author(snippet)
if Gitlab::UserAccessSnippet.new(snippet.author, snippet: snippet).can_do_action?(:update_snippet)
snippet.author
else
admin_user
end
end
def admin_user
@admin_user ||= User.admins.active.first
end
end
end
......
......@@ -21041,6 +21041,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
msgid "Third Party Advisory Link"
msgstr ""
msgid "Third party offers"
msgstr ""
......
......@@ -4,5 +4,10 @@ FactoryBot.define do
factory :remote_mirror, class: 'RemoteMirror' do
association :project, :repository
url { "http://foo:bar@test.com" }
trait :ssh do
url { 'ssh://git@test.com:foo/bar.git' }
auth_method { 'ssh_public_key' }
end
end
end
/* eslint-disable class-methods-use-this */
export default class WebWorkerMock {
addEventListener() {}
removeEventListener() {}
terminate() {}
postMessage() {}
}
......@@ -26,7 +26,7 @@ describe('IDE activity bar', () => {
describe('updateActivityBarView', () => {
beforeEach(() => {
spyOn(vm, 'updateActivityBarView');
jest.spyOn(vm, 'updateActivityBarView').mockImplementation(() => {});
vm.$mount();
});
......
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/ide/stores';
import Bar from '~/ide/components/file_templates/bar.vue';
import { resetStore, file } from '../../helpers';
......@@ -35,7 +35,7 @@ describe('IDE file templates bar component', () => {
});
it('calls setSelectedTemplateType when clicking item', () => {
spyOn(vm, 'setSelectedTemplateType').and.stub();
jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation();
vm.$el.querySelector('.dropdown-content button').click();
......@@ -66,7 +66,7 @@ describe('IDE file templates bar component', () => {
});
it('calls fetchTemplate on click', () => {
spyOn(vm, 'fetchTemplate').and.stub();
jest.spyOn(vm, 'fetchTemplate').mockImplementation();
vm.$el
.querySelectorAll('.dropdown-content')[1]
......@@ -90,7 +90,7 @@ describe('IDE file templates bar component', () => {
});
it('calls undoFileTemplate when clicking undo button', () => {
spyOn(vm, 'undoFileTemplate').and.stub();
jest.spyOn(vm, 'undoFileTemplate').mockImplementation();
vm.$el.querySelector('.btn-default').click();
......@@ -100,7 +100,7 @@ describe('IDE file templates bar component', () => {
it('calls setSelectedTemplateType if activeFile name matches a template', done => {
const fileName = '.gitlab-ci.yml';
spyOn(vm, 'setSelectedTemplateType');
jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation(() => {});
vm.$store.state.openFiles[0].name = fileName;
vm.setInitialType();
......
import Vue from 'vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import store from '~/ide/stores';
import ideSidebar from '~/ide/components/ide_side_bar.vue';
import { leftSidebarViews } from '~/ide/constants';
......
import Vue from 'vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import store from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
import { file, resetStore } from '../helpers';
......
......@@ -35,7 +35,7 @@ describe('IDE tree list', () => {
beforeEach(() => {
bootstrapWithTree();
spyOn(vm, 'updateViewer').and.callThrough();
jest.spyOn(vm, 'updateViewer');
vm.$mount();
});
......@@ -64,7 +64,7 @@ describe('IDE tree list', () => {
beforeEach(() => {
bootstrapWithTree(emptyBranchTree);
spyOn(vm, 'updateViewer').and.callThrough();
jest.spyOn(vm, 'updateViewer');
vm.$mount();
});
......
import Vue from 'vue';
import { trimText } from 'spec/helpers/text_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { trimText } from 'helpers/text_helper';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
import { createStore } from '~/ide/stores';
......
import $ from 'jquery';
import { mount } from '@vue/test-utils';
import { createStore } from '~/ide/stores';
import NavDropdown from '~/ide/components/nav_dropdown.vue';
import { PERMISSION_READ_MR } from '~/ide/constants';
const TEST_PROJECT_ID = 'lorem-ipsum';
describe('IDE NavDropdown', () => {
let store;
let wrapper;
beforeEach(() => {
store = createStore();
Object.assign(store.state, {
currentProjectId: TEST_PROJECT_ID,
currentBranchId: 'master',
projects: {
[TEST_PROJECT_ID]: {
userPermissions: {
[PERMISSION_READ_MR]: true,
},
branches: {
master: { id: 'master' },
},
},
},
});
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
});
afterEach(() => {
wrapper.destroy();
});
const createComponent = () => {
wrapper = mount(NavDropdown, {
store,
});
};
const findIcon = name => wrapper.find(`.ic-${name}`);
const findMRIcon = () => findIcon('merge-request');
const findNavForm = () => wrapper.find('.ide-nav-form');
const showDropdown = () => {
$(wrapper.vm.$el).trigger('show.bs.dropdown');
};
const hideDropdown = () => {
$(wrapper.vm.$el).trigger('hide.bs.dropdown');
};
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('renders nothing initially', () => {
expect(findNavForm().exists()).toBe(false);
});
it('renders nav form when show.bs.dropdown', done => {
showDropdown();
wrapper.vm
.$nextTick()
.then(() => {
expect(findNavForm().exists()).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('destroys nav form when closed', done => {
showDropdown();
hideDropdown();
wrapper.vm
.$nextTick()
.then(() => {
expect(findNavForm().exists()).toBe(false);
})
.then(done)
.catch(done.fail);
});
it('renders merge request icon', () => {
expect(findMRIcon().exists()).toBe(true);
});
});
describe('when user cannot read merge requests', () => {
beforeEach(() => {
store.state.projects[TEST_PROJECT_ID].userPermissions = {};
createComponent();
});
it('does not render merge requests', () => {
expect(findMRIcon().exists()).toBe(false);
});
});
});
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import Button from '~/ide/components/new_dropdown/button.vue';
describe('IDE new entry dropdown button component', () => {
......@@ -16,7 +16,7 @@ describe('IDE new entry dropdown button component', () => {
icon: 'doc-new',
});
spyOn(vm, '$emit');
jest.spyOn(vm, '$emit').mockImplementation(() => {});
});
afterEach(() => {
......
import Vue from 'vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import store from '~/ide/stores';
import newDropdown from '~/ide/components/new_dropdown/index.vue';
import { resetStore } from '../../helpers';
......@@ -23,7 +23,7 @@ describe('new dropdown component', () => {
tree: [],
};
spyOn(vm, 'openNewEntryModal');
jest.spyOn(vm, 'openNewEntryModal').mockImplementation(() => {});
vm.$mount();
});
......@@ -58,11 +58,11 @@ describe('new dropdown component', () => {
describe('isOpen', () => {
it('scrolls dropdown into view', done => {
spyOn(vm.$refs.dropdownMenu, 'scrollIntoView');
jest.spyOn(vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
vm.isOpen = true;
setTimeout(() => {
setImmediate(() => {
expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
block: 'nearest',
});
......@@ -74,7 +74,7 @@ describe('new dropdown component', () => {
describe('delete entry', () => {
it('calls delete action', () => {
spyOn(vm, 'deleteEntry');
jest.spyOn(vm, 'deleteEntry').mockImplementation(() => {});
vm.$el.querySelectorAll('.dropdown-menu button')[4].click();
......
import Vue from 'vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/ide/stores';
import modal from '~/ide/components/new_dropdown/modal.vue';
import createFlash from '~/flash';
jest.mock('~/flash');
describe('new file modal component', () => {
const Component = Vue.extend(modal);
......@@ -11,47 +14,45 @@ describe('new file modal component', () => {
vm.$destroy();
});
['tree', 'blob'].forEach(type => {
describe(type, () => {
beforeEach(() => {
const store = createStore();
store.state.entryModal = {
type,
describe.each(['tree', 'blob'])('%s', type => {
beforeEach(() => {
const store = createStore();
store.state.entryModal = {
type,
path: '',
entry: {
path: '',
entry: {
path: '',
},
};
},
};
vm = createComponentWithStore(Component, store).$mount();
vm = createComponentWithStore(Component, store).$mount();
vm.name = 'testing';
});
vm.name = 'testing';
});
it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
it(`sets modal title as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
});
expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
});
it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
it(`sets button label as ${type}`, () => {
const title = type === 'tree' ? 'directory' : 'file';
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
});
expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
});
it(`sets form label as ${type}`, () => {
expect(vm.$el.querySelector('.label-bold').textContent.trim()).toBe('Name');
});
it(`sets form label as ${type}`, () => {
expect(vm.$el.querySelector('.label-bold').textContent.trim()).toBe('Name');
});
it(`${type === 'tree' ? 'does not show' : 'shows'} file templates`, () => {
const templateFilesEl = vm.$el.querySelector('.file-templates');
if (type === 'tree') {
expect(templateFilesEl).toBeNull();
} else {
expect(templateFilesEl instanceof Element).toBeTruthy();
}
});
it(`${type === 'tree' ? 'does not show' : 'shows'} file templates`, () => {
const templateFilesEl = vm.$el.querySelector('.file-templates');
if (type === 'tree') {
expect(templateFilesEl).toBeNull();
} else {
expect(templateFilesEl instanceof Element).toBeTruthy();
}
});
});
......@@ -131,16 +132,15 @@ describe('new file modal component', () => {
};
vm = createComponentWithStore(Component, store).$mount();
const flashSpy = spyOnDependency(modal, 'flash');
expect(flashSpy).not.toHaveBeenCalled();
expect(createFlash).not.toHaveBeenCalled();
vm.submitForm();
expect(flashSpy).toHaveBeenCalledWith(
expect(createFlash).toHaveBeenCalledWith(
'The name "test-path/test" is already taken in this directory.',
'alert',
jasmine.anything(),
expect.anything(),
null,
false,
true,
......
......@@ -17,7 +17,7 @@ describe('RepoTab', () => {
}
beforeEach(() => {
spyOn(router, 'push');
jest.spyOn(router, 'push').mockImplementation(() => {});
});
afterEach(() => {
......@@ -47,7 +47,7 @@ describe('RepoTab', () => {
},
});
spyOn(vm, 'openPendingTab');
jest.spyOn(vm, 'openPendingTab').mockImplementation(() => {});
vm.$el.click();
......@@ -63,7 +63,7 @@ describe('RepoTab', () => {
tab: file(),
});
spyOn(vm, 'clickFile');
jest.spyOn(vm, 'clickFile').mockImplementation(() => {});
vm.$el.click();
......@@ -75,7 +75,7 @@ describe('RepoTab', () => {
tab: file(),
});
spyOn(vm, 'closeFile');
jest.spyOn(vm, 'closeFile').mockImplementation(() => {});
vm.$el.querySelector('.multi-file-tab-close').click();
......
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import TokenedInput from '~/ide/components/shared/tokened_input.vue';
const TEST_PLACEHOLDER = 'Searching in test';
......@@ -36,7 +36,7 @@ describe('IDE shared/TokenedInput', () => {
value: TEST_VALUE,
});
spyOn(vm, '$emit');
jest.spyOn(vm, '$emit').mockImplementation(() => {});
});
afterEach(() => {
......@@ -72,7 +72,7 @@ describe('IDE shared/TokenedInput', () => {
});
it('when input triggers backspace event, it calls "onBackspace"', () => {
spyOn(vm, 'onBackspace');
jest.spyOn(vm, 'onBackspace').mockImplementation(() => {});
vm.$refs.input.dispatchEvent(createBackspaceEvent());
vm.$refs.input.dispatchEvent(createBackspaceEvent());
......
......@@ -28,7 +28,7 @@ describe('Multi-file editor library model manager', () => {
});
it('adds model into disposable', () => {
spyOn(instance.disposable, 'add').and.callThrough();
jest.spyOn(instance.disposable, 'add');
instance.addModel(file());
......@@ -36,7 +36,7 @@ describe('Multi-file editor library model manager', () => {
});
it('returns cached model', () => {
spyOn(instance.models, 'get').and.callThrough();
jest.spyOn(instance.models, 'get');
instance.addModel(file());
instance.addModel(file());
......@@ -46,13 +46,13 @@ describe('Multi-file editor library model manager', () => {
it('adds eventHub listener', () => {
const f = file();
spyOn(eventHub, '$on').and.callThrough();
jest.spyOn(eventHub, '$on');
instance.addModel(f);
expect(eventHub.$on).toHaveBeenCalledWith(
`editor.update.model.dispose.${f.key}`,
jasmine.anything(),
expect.anything(),
);
});
});
......@@ -95,13 +95,13 @@ describe('Multi-file editor library model manager', () => {
});
it('removes eventHub listener', () => {
spyOn(eventHub, '$off').and.callThrough();
jest.spyOn(eventHub, '$off');
instance.removeCachedModel(f);
expect(eventHub.$off).toHaveBeenCalledWith(
`editor.update.model.dispose.${f.key}`,
jasmine.anything(),
expect.anything(),
);
});
});
......@@ -116,7 +116,7 @@ describe('Multi-file editor library model manager', () => {
});
it('calls disposable dispose', () => {
spyOn(instance.disposable, 'dispose').and.callThrough();
jest.spyOn(instance.disposable, 'dispose');
instance.dispose();
......
......@@ -6,7 +6,7 @@ describe('Multi-file editor library model', () => {
let model;
beforeEach(() => {
spyOn(eventHub, '$on').and.callThrough();
jest.spyOn(eventHub, '$on');
const f = file('path');
f.mrChange = { diff: 'ABC' };
......@@ -44,7 +44,7 @@ describe('Multi-file editor library model', () => {
it('adds eventHub listener', () => {
expect(eventHub.$on).toHaveBeenCalledWith(
`editor.update.model.dispose.${model.file.key}`,
jasmine.anything(),
expect.anything(),
);
});
......@@ -82,13 +82,13 @@ describe('Multi-file editor library model', () => {
describe('onChange', () => {
it('calls callback on change', done => {
const spy = jasmine.createSpy();
const spy = jest.fn();
model.onChange(spy);
model.getModel().setValue('123');
setTimeout(() => {
expect(spy).toHaveBeenCalledWith(model, jasmine.anything());
setImmediate(() => {
expect(spy).toHaveBeenCalledWith(model, expect.anything());
done();
});
});
......@@ -96,7 +96,7 @@ describe('Multi-file editor library model', () => {
describe('dispose', () => {
it('calls disposable dispose', () => {
spyOn(model.disposable, 'dispose').and.callThrough();
jest.spyOn(model.disposable, 'dispose');
model.dispose();
......@@ -114,18 +114,18 @@ describe('Multi-file editor library model', () => {
});
it('removes eventHub listener', () => {
spyOn(eventHub, '$off').and.callThrough();
jest.spyOn(eventHub, '$off');
model.dispose();
expect(eventHub.$off).toHaveBeenCalledWith(
`editor.update.model.dispose.${model.file.key}`,
jasmine.anything(),
expect.anything(),
);
});
it('calls onDispose callback', () => {
const disposeSpy = jasmine.createSpy();
const disposeSpy = jest.fn();
model.onDispose(disposeSpy);
......
......@@ -60,7 +60,7 @@ describe('Multi-file editor library decorations controller', () => {
});
it('calls decorate method', () => {
spyOn(controller, 'decorate');
jest.spyOn(controller, 'decorate').mockImplementation(() => {});
controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
......@@ -70,7 +70,7 @@ describe('Multi-file editor library decorations controller', () => {
describe('decorate', () => {
it('sets decorations on editor instance', () => {
spyOn(controller.editor.instance, 'deltaDecorations');
jest.spyOn(controller.editor.instance, 'deltaDecorations').mockImplementation(() => {});
controller.decorate(model);
......@@ -78,7 +78,7 @@ describe('Multi-file editor library decorations controller', () => {
});
it('caches decorations', () => {
spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
jest.spyOn(controller.editor.instance, 'deltaDecorations').mockReturnValue([]);
controller.decorate(model);
......@@ -86,7 +86,7 @@ describe('Multi-file editor library decorations controller', () => {
});
it('caches decorations by model URL', () => {
spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
jest.spyOn(controller.editor.instance, 'deltaDecorations').mockReturnValue([]);
controller.decorate(model);
......
......@@ -75,7 +75,7 @@ describe('Multi-file editor library dirty diff controller', () => {
describe('attachModel', () => {
it('adds change event callback', () => {
spyOn(model, 'onChange');
jest.spyOn(model, 'onChange').mockImplementation(() => {});
controller.attachModel(model);
......@@ -83,7 +83,7 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('adds dispose event callback', () => {
spyOn(model, 'onDispose');
jest.spyOn(model, 'onDispose').mockImplementation(() => {});
controller.attachModel(model);
......@@ -91,7 +91,7 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('calls throttledComputeDiff on change', () => {
spyOn(controller, 'throttledComputeDiff');
jest.spyOn(controller, 'throttledComputeDiff').mockImplementation(() => {});
controller.attachModel(model);
......@@ -109,7 +109,7 @@ describe('Multi-file editor library dirty diff controller', () => {
describe('computeDiff', () => {
it('posts to worker', () => {
spyOn(controller.dirtyDiffWorker, 'postMessage');
jest.spyOn(controller.dirtyDiffWorker, 'postMessage').mockImplementation(() => {});
controller.computeDiff(model);
......@@ -123,7 +123,7 @@ describe('Multi-file editor library dirty diff controller', () => {
describe('reDecorate', () => {
it('calls computeDiff when no decorations are cached', () => {
spyOn(controller, 'computeDiff');
jest.spyOn(controller, 'computeDiff').mockImplementation(() => {});
controller.reDecorate(model);
......@@ -131,7 +131,7 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('calls decorate when decorations are cached', () => {
spyOn(controller.decorationsController, 'decorate');
jest.spyOn(controller.decorationsController, 'decorate').mockImplementation(() => {});
controller.decorationsController.decorations.set(model.url, 'test');
......@@ -143,19 +143,19 @@ describe('Multi-file editor library dirty diff controller', () => {
describe('decorate', () => {
it('adds decorations into decorations controller', () => {
spyOn(controller.decorationsController, 'addDecorations');
jest.spyOn(controller.decorationsController, 'addDecorations').mockImplementation(() => {});
controller.decorate({ data: { changes: [], path: model.path } });
expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(
model,
'dirtyDiff',
jasmine.anything(),
expect.anything(),
);
});
it('adds decorations into editor', () => {
const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
const spy = jest.spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
controller.decorate({
data: { changes: computeDiff('123', '1234'), path: model.path },
......@@ -178,7 +178,7 @@ describe('Multi-file editor library dirty diff controller', () => {
describe('dispose', () => {
it('calls disposable dispose', () => {
spyOn(controller.disposable, 'dispose').and.callThrough();
jest.spyOn(controller.disposable, 'dispose');
controller.dispose();
......@@ -186,7 +186,7 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('terminates worker', () => {
spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
jest.spyOn(controller.dirtyDiffWorker, 'terminate');
controller.dispose();
......@@ -194,13 +194,13 @@ describe('Multi-file editor library dirty diff controller', () => {
});
it('removes worker event listener', () => {
spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
jest.spyOn(controller.dirtyDiffWorker, 'removeEventListener');
controller.dispose();
expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith(
'message',
jasmine.anything(),
expect.anything(),
);
});
......
import { editor as monacoEditor } from 'monaco-editor';
import Editor from '~/ide/lib/editor';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { file } from '../helpers';
describe('Multi-file editor library', () => {
......@@ -7,6 +8,14 @@ describe('Multi-file editor library', () => {
let el;
let holder;
const setNodeOffsetWidth = val => {
Object.defineProperty(instance.instance.getDomNode(), 'offsetWidth', {
get() {
return val;
},
});
};
beforeEach(() => {
el = document.createElement('div');
holder = document.createElement('div');
......@@ -18,7 +27,9 @@ describe('Multi-file editor library', () => {
});
afterEach(() => {
instance.modelManager.dispose();
instance.dispose();
Editor.editorInstance = null;
el.remove();
});
......@@ -33,7 +44,7 @@ describe('Multi-file editor library', () => {
describe('createInstance', () => {
it('creates editor instance', () => {
spyOn(monacoEditor, 'create').and.callThrough();
jest.spyOn(monacoEditor, 'create');
instance.createInstance(holder);
......@@ -55,33 +66,25 @@ describe('Multi-file editor library', () => {
describe('createDiffInstance', () => {
it('creates editor instance', () => {
spyOn(monacoEditor, 'createDiffEditor').and.callThrough();
jest.spyOn(monacoEditor, 'createDiffEditor');
instance.createDiffInstance(holder);
expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, {
model: null,
contextmenu: true,
minimap: {
enabled: false,
},
readOnly: true,
scrollBeyondLastLine: false,
renderWhitespace: 'none',
...defaultEditorOptions,
quickSuggestions: false,
occurrencesHighlight: false,
wordWrap: 'on',
renderSideBySide: true,
renderSideBySide: false,
readOnly: true,
renderLineHighlight: 'all',
hideCursorInOverviewRuler: false,
theme: 'vs white',
});
});
});
describe('createModel', () => {
it('calls model manager addModel', () => {
spyOn(instance.modelManager, 'addModel');
jest.spyOn(instance.modelManager, 'addModel').mockImplementation(() => {});
instance.createModel('FILE');
......@@ -105,7 +108,7 @@ describe('Multi-file editor library', () => {
});
it('attaches the model to the current instance', () => {
spyOn(instance.instance, 'setModel');
jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
instance.attachModel(model);
......@@ -113,8 +116,8 @@ describe('Multi-file editor library', () => {
});
it('sets original & modified when diff editor', () => {
spyOn(instance.instance, 'getEditorType').and.returnValue('vs.editor.IDiffEditor');
spyOn(instance.instance, 'setModel');
jest.spyOn(instance.instance, 'getEditorType').mockReturnValue('vs.editor.IDiffEditor');
jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
instance.attachModel(model);
......@@ -125,7 +128,7 @@ describe('Multi-file editor library', () => {
});
it('attaches the model to the dirty diff controller', () => {
spyOn(instance.dirtyDiffController, 'attachModel');
jest.spyOn(instance.dirtyDiffController, 'attachModel').mockImplementation(() => {});
instance.attachModel(model);
......@@ -133,7 +136,7 @@ describe('Multi-file editor library', () => {
});
it('re-decorates with the dirty diff controller', () => {
spyOn(instance.dirtyDiffController, 'reDecorate');
jest.spyOn(instance.dirtyDiffController, 'reDecorate').mockImplementation(() => {});
instance.attachModel(model);
......@@ -155,7 +158,7 @@ describe('Multi-file editor library', () => {
});
it('sets original & modified', () => {
spyOn(instance.instance, 'setModel');
jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
instance.attachMergeRequestModel(model);
......@@ -170,7 +173,7 @@ describe('Multi-file editor library', () => {
it('resets the editor model', () => {
instance.createInstance(document.createElement('div'));
spyOn(instance.instance, 'setModel');
jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
instance.clearEditor();
......@@ -180,7 +183,7 @@ describe('Multi-file editor library', () => {
describe('dispose', () => {
it('calls disposble dispose method', () => {
spyOn(instance.disposable, 'dispose').and.callThrough();
jest.spyOn(instance.disposable, 'dispose');
instance.dispose();
......@@ -198,7 +201,7 @@ describe('Multi-file editor library', () => {
});
it('does not dispose modelManager', () => {
spyOn(instance.modelManager, 'dispose');
jest.spyOn(instance.modelManager, 'dispose').mockImplementation(() => {});
instance.dispose();
......@@ -206,7 +209,7 @@ describe('Multi-file editor library', () => {
});
it('does not dispose decorationsController', () => {
spyOn(instance.decorationsController, 'dispose');
jest.spyOn(instance.decorationsController, 'dispose').mockImplementation(() => {});
instance.dispose();
......@@ -219,7 +222,7 @@ describe('Multi-file editor library', () => {
it('does not update options', () => {
instance.createInstance(holder);
spyOn(instance.instance, 'updateOptions');
jest.spyOn(instance.instance, 'updateOptions').mockImplementation(() => {});
instance.updateDiffView();
......@@ -231,11 +234,11 @@ describe('Multi-file editor library', () => {
beforeEach(() => {
instance.createDiffInstance(holder);
spyOn(instance.instance, 'updateOptions').and.callThrough();
jest.spyOn(instance.instance, 'updateOptions');
});
it('sets renderSideBySide to false if el is less than 700 pixels', () => {
spyOnProperty(instance.instance.getDomNode(), 'offsetWidth').and.returnValue(600);
setNodeOffsetWidth(600);
expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({
renderSideBySide: false,
......@@ -243,7 +246,7 @@ describe('Multi-file editor library', () => {
});
it('sets renderSideBySide to false if el is more than 700 pixels', () => {
spyOnProperty(instance.instance.getDomNode(), 'offsetWidth').and.returnValue(800);
setNodeOffsetWidth(800);
expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({
renderSideBySide: true,
......@@ -269,7 +272,7 @@ describe('Multi-file editor library', () => {
it('sets quickSuggestions to false when language is markdown', () => {
instance.createInstance(holder);
spyOn(instance.instance, 'updateOptions').and.callThrough();
jest.spyOn(instance.instance, 'updateOptions');
const model = instance.createModel({
...file(),
......
/* eslint-disable class-methods-use-this */
export default class TreeWorkerMock {
addEventListener() {}
terminate() {}
postMessage() {}
}
export { default } from 'helpers/web_worker_mock';
export { default } from 'helpers/web_worker_mock';
import $ from 'jquery';
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import store from '~/ide/stores';
import NavDropdown from '~/ide/components/nav_dropdown.vue';
import { PERMISSION_READ_MR } from '~/ide/constants';
const TEST_PROJECT_ID = 'lorem-ipsum';
describe('IDE NavDropdown', () => {
const Component = Vue.extend(NavDropdown);
let vm;
let $dropdown;
beforeEach(() => {
store.state.currentProjectId = TEST_PROJECT_ID;
Vue.set(store.state.projects, TEST_PROJECT_ID, {
userPermissions: {
[PERMISSION_READ_MR]: true,
},
});
vm = mountComponentWithStore(Component, { store });
$dropdown = $(vm.$el);
// block dispatch from doing anything
spyOn(vm.$store, 'dispatch');
});
afterEach(() => {
vm.$destroy();
});
const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
const findMRIcon = () => findIcon('merge-request');
it('renders nothing initially', () => {
expect(vm.$el).not.toContainElement('.ide-nav-form');
});
it('renders nav form when show.bs.dropdown', done => {
$dropdown.trigger('show.bs.dropdown');
vm.$nextTick()
.then(() => {
expect(vm.$el).toContainElement('.ide-nav-form');
})
.then(done)
.catch(done.fail);
});
it('destroys nav form when closed', done => {
$dropdown.trigger('show.bs.dropdown');
$dropdown.trigger('hide.bs.dropdown');
vm.$nextTick()
.then(() => {
expect(vm.$el).not.toContainElement('.ide-nav-form');
})
.then(done)
.catch(done.fail);
});
it('renders merge request icon', () => {
expect(findMRIcon()).not.toBeNull();
});
describe('when user cannot read merge requests', () => {
beforeEach(done => {
store.state.projects[TEST_PROJECT_ID].userPermissions = {};
vm.$nextTick()
.then(done)
.catch(done.fail);
});
it('does not render merge requests', () => {
expect(findMRIcon()).toBeNull();
});
});
});
......@@ -2,13 +2,30 @@
require 'spec_helper'
describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 2020_02_26_162723 do
describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 2020_04_20_094444 do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:users) { table(:users) }
let(:snippets) { table(:snippets) }
let(:snippet_repositories) { table(:snippet_repositories) }
let(:user) { users.create(id: 1, email: 'user@example.com', projects_limit: 10, username: 'test', name: 'Test') }
let(:user_state) { 'active' }
let(:ghost) { false }
let(:user_type) { nil }
let!(:user) do
users.create(id: 1,
email: 'user@example.com',
projects_limit: 10,
username: 'test',
name: 'Test',
state: user_state,
ghost: ghost,
last_activity_on: 1.minute.ago,
user_type: user_type,
confirmed_at: 1.day.ago)
end
let!(:admin) { users.create(id: 2, email: 'admin@example.com', projects_limit: 10, username: 'admin', name: 'Admin', admin: true, state: 'active') }
let!(:snippet_with_repo) { snippets.create(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let!(:snippet_with_empty_repo) { snippets.create(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let!(:snippet_without_repo) { snippets.create(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
......@@ -54,14 +71,51 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
end
shared_examples 'commits the file to the repository' do
it do
subject
context 'when author can update snippet and use git' do
it 'creates the repository and commit the file' do
subject
blob = blob_at(snippet, file_name)
last_commit = raw_repository(snippet).commit
aggregate_failures do
expect(blob).to be
expect(blob.data).to eq content
expect(last_commit.author_name).to eq user.name
expect(last_commit.author_email).to eq user.email
end
end
end
blob = blob_at(snippet, file_name)
context 'when author cannot update snippet or use git' do
shared_examples 'admin user commits files' do
it do
subject
aggregate_failures do
expect(blob).to be
expect(blob.data).to eq content
last_commit = raw_repository(snippet).commit
expect(last_commit.author_name).to eq admin.name
expect(last_commit.author_email).to eq admin.email
end
end
context 'when user is blocked' do
let(:user_state) { 'blocked' }
it_behaves_like 'admin user commits files'
end
context 'when user is deactivated' do
let(:user_state) { 'deactivated' }
it_behaves_like 'admin user commits files'
end
context 'when user is a ghost' do
let(:ghost) { true }
let(:user_type) { 'ghost' }
it_behaves_like 'admin user commits files'
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200420094444_backfill_snippet_repositories.rb')
describe BackfillSnippetRepositories do
let(:users) { table(:users) }
let(:snippets) { table(:snippets) }
let(:user) { users.create(id: 1, email: 'user@example.com', projects_limit: 10, username: 'test', name: 'Test', state: 'active') }
def create_snippet(id)
params = {
id: id,
type: 'PersonalSnippet',
author_id: user.id,
file_name: 'foo',
content: 'bar'
}
snippets.create!(params)
end
it 'correctly schedules background migrations' do
create_snippet(1)
create_snippet(2)
create_snippet(3)
stub_const("#{described_class.name}::BATCH_SIZE", 2)
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(3.minutes, 1, 2)
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(6.minutes, 3, 3)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end
......@@ -143,22 +143,54 @@ describe RemoteMirror, :mailer do
end
describe '#update_repository' do
let(:git_remote_mirror) { spy }
it 'performs update including options' do
git_remote_mirror = stub_const('Gitlab::Git::RemoteMirror', spy)
mirror = build(:remote_mirror)
before do
stub_const('Gitlab::Git::RemoteMirror', git_remote_mirror)
expect(mirror).to receive(:options_for_update).and_return(options: true)
mirror.update_repository
expect(git_remote_mirror).to have_received(:new).with(
mirror.project.repository.raw,
mirror.remote_name,
options: true
)
expect(git_remote_mirror).to have_received(:update)
end
end
it 'includes the `keep_divergent_refs` setting' do
describe '#options_for_update' do
it 'includes the `keep_divergent_refs` option' do
mirror = build_stubbed(:remote_mirror, keep_divergent_refs: true)
mirror.update_repository({})
options = mirror.options_for_update
expect(git_remote_mirror).to have_received(:new).with(
anything,
mirror.remote_name,
hash_including(keep_divergent_refs: true)
)
expect(options).to include(keep_divergent_refs: true)
end
it 'includes the `only_branches_matching` option' do
branch = create(:protected_branch)
mirror = build_stubbed(:remote_mirror, project: branch.project, only_protected_branches: true)
options = mirror.options_for_update
expect(options).to include(only_branches_matching: [branch.name])
end
it 'includes the `ssh_key` option' do
mirror = build(:remote_mirror, :ssh, ssh_private_key: 'private-key')
options = mirror.options_for_update
expect(options).to include(ssh_key: 'private-key')
end
it 'includes the `known_hosts` option' do
mirror = build(:remote_mirror, :ssh, ssh_known_hosts: 'known-hosts')
options = mirror.options_for_update
expect(options).to include(known_hosts: 'known-hosts')
end
end
......
......@@ -6,9 +6,10 @@ describe 'Destroying a Snippet' do
include GraphqlHelpers
let(:current_user) { snippet.author }
let(:snippet_gid) { snippet.to_global_id.to_s }
let(:mutation) do
variables = {
id: snippet.to_global_id.to_s
id: snippet_gid
}
graphql_mutation(:destroy_snippet, variables)
......@@ -49,9 +50,11 @@ describe 'Destroying a Snippet' do
end
describe 'PersonalSnippet' do
it_behaves_like 'graphql delete actions' do
let_it_be(:snippet) { create(:personal_snippet) }
end
let_it_be(:snippet) { create(:personal_snippet) }
it_behaves_like 'graphql delete actions'
it_behaves_like 'when the snippet is not found'
end
describe 'ProjectSnippet' do
......@@ -85,5 +88,7 @@ describe 'Destroying a Snippet' do
end
end
end
it_behaves_like 'when the snippet is not found'
end
end
......@@ -10,9 +10,11 @@ describe 'Mark snippet as spam', :do_not_mock_admin_mode do
let_it_be(:snippet) { create(:personal_snippet) }
let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: snippet) }
let(:current_user) { snippet.author }
let(:snippet_gid) { snippet.to_global_id.to_s }
let(:mutation) do
variables = {
id: snippet.to_global_id.to_s
id: snippet_gid
}
graphql_mutation(:mark_as_spam_snippet, variables)
......@@ -30,6 +32,8 @@ describe 'Mark snippet as spam', :do_not_mock_admin_mode do
end
end
it_behaves_like 'when the snippet is not found'
context 'when the user does not have permission' do
let(:current_user) { other_user }
......
......@@ -15,9 +15,10 @@ describe 'Updating a Snippet' do
let(:updated_file_name) { 'Updated file_name' }
let(:current_user) { snippet.author }
let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s }
let(:mutation) do
variables = {
id: GitlabSchema.id_from_object(snippet).to_s,
id: snippet_gid,
content: updated_content,
description: updated_description,
visibility_level: 'public',
......@@ -90,16 +91,18 @@ describe 'Updating a Snippet' do
end
describe 'PersonalSnippet' do
it_behaves_like 'graphql update actions' do
let(:snippet) do
create(:personal_snippet,
:private,
file_name: original_file_name,
title: original_title,
content: original_content,
description: original_description)
end
let(:snippet) do
create(:personal_snippet,
:private,
file_name: original_file_name,
title: original_title,
content: original_content,
description: original_description)
end
it_behaves_like 'graphql update actions'
it_behaves_like 'when the snippet is not found'
end
describe 'ProjectSnippet' do
......@@ -142,5 +145,7 @@ describe 'Updating a Snippet' do
end
end
end
it_behaves_like 'when the snippet is not found'
end
end
......@@ -5,7 +5,7 @@ require 'spec_helper'
describe Projects::UpdateRemoteMirrorService do
let(:project) { create(:project, :repository) }
let(:remote_project) { create(:forked_project_with_submodules) }
let(:remote_mirror) { project.remote_mirrors.create!(url: remote_project.http_url_to_repo, enabled: true, only_protected_branches: false) }
let(:remote_mirror) { create(:remote_mirror, project: project, enabled: true) }
let(:remote_name) { remote_mirror.remote_name }
subject(:service) { described_class.new(project, project.creator) }
......@@ -16,7 +16,9 @@ describe Projects::UpdateRemoteMirrorService do
before do
project.repository.add_branch(project.owner, 'existing-branch', 'master')
allow(remote_mirror).to receive(:update_repository).and_return(true)
allow(remote_mirror)
.to receive(:update_repository)
.and_return(double(divergent_refs: []))
end
it 'ensures the remote exists' do
......@@ -53,7 +55,7 @@ describe Projects::UpdateRemoteMirrorService do
it 'marks the mirror as failed and raises the error when an unexpected error occurs' do
allow(project.repository).to receive(:fetch_remote).and_raise('Badly broken')
expect { execute! }.to raise_error /Badly broken/
expect { execute! }.to raise_error(/Badly broken/)
expect(remote_mirror).to be_failed
expect(remote_mirror.last_error).to include('Badly broken')
......@@ -83,32 +85,21 @@ describe Projects::UpdateRemoteMirrorService do
end
end
context 'when syncing all branches' do
it 'push all the branches the first time' do
context 'when there are divergent refs' do
before do
stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror)
expect(remote_mirror).to receive(:update_repository).with({})
execute!
end
end
context 'when only syncing protected branches' do
it 'sync updated protected branches' do
stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror)
protected_branch = create_protected_branch(project)
remote_mirror.only_protected_branches = true
expect(remote_mirror)
.to receive(:update_repository)
.with(only_branches_matching: [protected_branch.name])
it 'marks the mirror as failed and sets an error message' do
response = double(divergent_refs: %w[refs/heads/master refs/heads/develop])
expect(remote_mirror).to receive(:update_repository).and_return(response)
execute!
end
def create_protected_branch(project)
branch_name = project.repository.branch_names.find { |n| n != 'existing-branch' }
create(:protected_branch, project: project, name: branch_name)
expect(remote_mirror).to be_failed
expect(remote_mirror.last_error).to include("Some refs have diverged")
expect(remote_mirror.last_error).to include("refs/heads/master\n")
expect(remote_mirror.last_error).to include("refs/heads/develop")
end
end
end
......
# frozen_string_literal: true
RSpec.shared_examples 'when the snippet is not found' do
let(:snippet_gid) do
"gid://gitlab/#{snippet.class.name}/#{non_existing_record_id}"
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册