Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-06 00:09:14 +00:00
parent 1bc5af7661
commit c93374099a
39 changed files with 682 additions and 335 deletions

View file

@ -96,7 +96,8 @@ In which enterprise tier should this feature go? See https://about.gitlab.com/ha
### Links / references
<!-- Label reminders - you should have one of each of the following labels if you can figure out the correct ones -->
<!-- Label reminders - you should have one of each of the following labels.
Read the descriptions on https://gitlab.com/gitlab-org/gitlab/-/labels to find the correct ones -->
/label ~devops:: ~group: ~Category:
/label ~feature

View file

@ -17,7 +17,7 @@
/label ~"feature" ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Starter"/~"GitLab Premium"/~"GitLab Ultimate"
<!-- Consider adding related issues and epics to this issue. You can also reference the Feature Proposal Template (https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md) for additional details to consider adding to this issue. Additionally, as a data oriented organization, when your feature exits planning breakdown, consider adding the `What does success look like, and how can we measure that?` section.
<!-- Read the labels descriptions on https://gitlab.com/gitlab-org/gitlab/-/labels to find the appropriate labels. Consider adding related issues and epics to this issue. You can also reference the Feature Proposal Template (https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md) for additional details to consider adding to this issue. Additionally, as a data oriented organization, when your feature exits planning breakdown, consider adding the `What does success look like, and how can we measure that?` section.
Other sections to consider adding:

View file

@ -150,7 +150,7 @@ module IssuableCollections
common_attributes + [:project, project: :namespace]
when 'MergeRequest'
common_attributes + [
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users,
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users, :reviewers,
source_project: :route, head_pipeline: :project, target_project: :namespace
]
end

View file

@ -406,7 +406,7 @@ class IssuableFinder
elsif params.filter_by_any_assignee?
items.assigned
elsif params.assignee
items.assigned_to(params.assignee)
items_assigned_to(items, params.assignee)
elsif params.assignee_id? || params.assignee_username? # assignee not found
items.none
else
@ -414,6 +414,10 @@ class IssuableFinder
end
end
def items_assigned_to(items, user)
items.assigned_to(user)
end
def by_negated_assignee(items)
# We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
if not_params.assignees.present?

View file

@ -164,6 +164,13 @@ class MergeRequestsFinder < IssuableFinder
end
# rubocop: enable CodeReuse/Finder
# rubocop: disable CodeReuse/ActiveRecord
def items_assigned_to(items, user)
assignee_or_reviewer = MergeRequest.from_union([super, items.reviewer_assigned_to(user)])
items.where(id: assignee_or_reviewer)
end
# rubocop: enable CodeReuse/ActiveRecord
def by_deployments(items)
env = params[:environment]
before = params[:deployed_before]

View file

@ -303,6 +303,19 @@ class MergeRequest < ApplicationRecord
includes(:metrics)
end
scope :reviewer_assigned_to, ->(user) do
mr_reviewers_table = MergeRequestReviewer.arel_table
inner_sql = mr_reviewers_table
.project(Arel::Nodes::True.new)
.where(
mr_reviewers_table[:merge_request_id].eq(MergeRequest.arel_table[:id])
.and(mr_reviewers_table[:user_id].eq(user.id))
).exists
where(inner_sql)
end
after_save :keep_around_commit, unless: :importing?
alias_attribute :project, :target_project

View file

@ -129,7 +129,7 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :security_reports_docs_path do |merge_request|
help_page_path('user/application_security/sast/index.md', anchor: 'reports-json-format')
help_page_path('user/application_security/index.md', anchor: 'viewing-security-scan-information-in-merge-requests')
end
private

View file

@ -0,0 +1,84 @@
# frozen_string_literal: true
module Issuable
module ImportCsv
class BaseService
def initialize(user, project, csv_io)
@user = user
@project = project
@csv_io = csv_io
@results = { success: 0, error_lines: [], parse_error: false }
end
def execute
process_csv
email_results_to_user
@results
end
private
def process_csv
with_csv_lines.each do |row, line_no|
issuable_attributes = {
title: row[:title],
description: row[:description]
}
if create_issuable(issuable_attributes).persisted?
@results[:success] += 1
else
@results[:error_lines].push(line_no)
end
end
rescue ArgumentError, CSV::MalformedCSVError
@results[:parse_error] = true
end
def with_csv_lines
csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8)
verify_headers!(csv_data)
csv_parsing_params = {
col_sep: detect_col_sep(csv_data.lines.first),
headers: true,
header_converters: :symbol
}
CSV.new(csv_data, csv_parsing_params).each.with_index(2)
end
def verify_headers!(data)
headers = data.lines.first.downcase
return if headers.include?('title') && headers.include?('description')
raise CSV::MalformedCSVError
end
def detect_col_sep(header)
if header.include?(",")
","
elsif header.include?(";")
";"
elsif header.include?("\t")
"\t"
else
raise CSV::MalformedCSVError
end
end
def create_issuable(attributes)
create_issuable_class.new(@project, @user, attributes).execute
end
def email_results_to_user
# defined in ImportCsvService
end
def create_issuable_class
# defined in ImportCsvService
end
end
end
end

View file

@ -1,69 +1,25 @@
# frozen_string_literal: true
module Issues
class ImportCsvService
def initialize(user, project, csv_io)
@user = user
@project = project
@csv_io = csv_io
@results = { success: 0, error_lines: [], parse_error: false }
end
class ImportCsvService < Issuable::ImportCsv::BaseService
def execute
record_import_attempt
process_csv
email_results_to_user
@results
end
private
def record_import_attempt
Issues::CsvImport.create!(user: @user, project: @project)
end
def process_csv
csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8)
csv_parsing_params = {
col_sep: detect_col_sep(csv_data.lines.first),
headers: true,
header_converters: :symbol
}
CSV.new(csv_data, csv_parsing_params).each.with_index(2) do |row, line_no|
issue_attributes = {
title: row[:title],
description: row[:description]
}
issue = Issues::CreateService.new(@project, @user, issue_attributes).execute
if issue.persisted?
@results[:success] += 1
else
@results[:error_lines].push(line_no)
end
end
rescue ArgumentError, CSV::MalformedCSVError
@results[:parse_error] = true
super
end
def email_results_to_user
Notify.import_issues_csv_email(@user.id, @project.id, @results).deliver_later
end
def detect_col_sep(header)
if header.include?(",")
","
elsif header.include?(";")
";"
elsif header.include?("\t")
"\t"
else
raise CSV::MalformedCSVError
end
private
def create_issuable_class
Issues::CreateService
end
def record_import_attempt
Issues::CsvImport.create!(user: @user, project: @project)
end
end
end

View file

@ -1,6 +1,6 @@
-# We currently only support `alert`, `notice`, `success`, 'toast'
- icons = {'alert' => 'error', 'notice' => 'information-o', 'success' => 'check-circle'};
.flash-container.flash-container-page.sticky
.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' } }
- flash.each do |key, value|
- if key == 'toast' && value
.js-toast-message{ data: { message: value } }

View file

@ -2,7 +2,7 @@
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/49713 for more information.
%ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects)
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown", track_value: "" } }) do
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do
%button{ type: 'button', data: { toggle: "dropdown" } }
= _('Projects')
= sprite_icon('chevron-down', css_class: 'caret-down')
@ -10,7 +10,7 @@
= render "layouts/nav/projects_dropdown/show"
- if dashboard_nav_link?(:groups)
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "d-none d-md-block home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown", track_value: "" } }) do
= nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "d-none d-md-block home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do
%button{ type: 'button', data: { toggle: "dropdown" } }
= _('Groups')
= sprite_icon('chevron-down', css_class: 'caret-down')
@ -18,7 +18,7 @@
= render "layouts/nav/groups_dropdown/show"
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
%li.header-more.dropdown{ **tracking_attrs('main_navigation', 'click_more_link', 'navigation') }
= nav_link(html_options: { id: 'nav-more-dropdown', class: "header-more dropdown", data: { track_label: "more_dropdown", track_event: "click_more_link" } }) do
%a{ href: "#", data: { toggle: "dropdown", qa_selector: 'more_dropdown' } }
= _('More')
= sprite_icon('chevron-down', css_class: 'caret-down')

View file

@ -1,7 +1,7 @@
.file-header-content
= blob_icon blob.mode, blob.name
%strong.file-title-name
%strong.file-title-name{ data: { qa_selector: 'file_name_content' } }
= blob.name
= copy_file_path_button(blob.path)

View file

@ -23,7 +23,7 @@
= author_avatar(commit, size: 40, has_tooltip: false)
.commit-detail.flex-list
.commit-content.qa-commit-content
.commit-content{ data: { qa_selector: 'commit_content' } }
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: ["commit-row-message item-title js-onboarding-commit-item", ("font-italic" if commit.message.empty?)]
- else

View file

@ -11,7 +11,7 @@
= link_icon
= i
- highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil
.blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight } }
.blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight, qa_selector: 'file_content' } }
%pre.code.highlight
%code
= blob.present.highlight

View file

@ -184,7 +184,7 @@ to authenticate with the API:
- [Maven Repository](../user/packages/maven_repository/index.md#authenticate-with-a-ci-job-token-in-maven)
- [NPM Repository](../user/packages/npm_registry/index.md#authenticate-with-a-ci-job-token)
- [Nuget Repository](../user/packages/nuget_repository/index.md)
- [PyPI Repository](../user/packages/pypi_repository/index.md#using-gitlab-ci-with-pypi-packages)
- [PyPI Repository](../user/packages/pypi_repository/index.md#publish-a-pypi-package-by-using-cicd)
- [Generic packages](../user/packages/generic_packages/index.md#publish-a-generic-package-by-using-cicd)
- [Get job artifacts](job_artifacts.md#get-job-artifacts)
- [Pipeline triggers](pipeline_triggers.md) (using the `token=` parameter)

View file

@ -362,13 +362,13 @@ Kubernetes:
##### Docker executor inside GitLab Runner configuration
If you are an administrator of GitLab Runner and you always want to use
If you are an administrator of GitLab Runner and you want to use
the mirror for every `dind` service, update the
[configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
to specify a [volume
mount](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#volumes-in-the-runnersdocker-section).
Given we have a file `/opt/docker/daemon.json` with the following
For example, if you have a `/opt/docker/daemon.json` file with the following
content:
```json
@ -379,7 +379,7 @@ content:
}
```
Update the `config.toml` for GitLab Runner to mount the file to
Update the `config.toml` file to mount the file to
`/etc/docker/daemon.json`. This would mount the file for **every**
container that is created by GitLab Runner. The configuration will be
picked up by the `dind` service.
@ -394,7 +394,57 @@ picked up by the `dind` service.
volumes = ["/opt/docker/daemon.json:/etc/docker/daemon.json:ro"]
```
### Use Docker socket binding
##### Kubernetes executor inside GitLab Runner configuration
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3223) in GitLab Runner 13.6.
If you are an administrator of GitLab Runner and you want to use
the mirror for every `dind` service, update the
[configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
to specify a [ConfigMap volume
mount](https://docs.gitlab.com/runner/executors/kubernetes.html#using-volumes).
For example, if you have a `/tmp/daemon.json` file with the following
content:
```json
{
"registry-mirrors": [
"https://registry-mirror.example.com"
]
}
```
Create a [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) with the content
of this file. You can do this with a command like:
```shell
kubectl create configmap docker-daemon --namespace gitlab-runner --from-file /tmp/daemon.json
```
NOTE: **Note:**
Make sure to use the namespace that GitLab Runner Kubernetes executor uses
to create job pods in.
After the ConfigMap is created, you can update the `config.toml`
file to mount the file to `/etc/docker/daemon.json`. This update
mounts the file for **every** container that is created by GitLab Runner.
The configuration is picked up by the `dind` service.
```toml
[[runners]]
...
executor = "kubernetes"
[runners.kubernetes]
image = "alpine:3.12"
privileged = true
[[runners.kubernetes.volumes.config_map]]
name = "docker-daemon"
mount_path = "/etc/docker/daemon.json"
sub_path = "daemon.json"
```
#### Use Docker socket binding
The third approach is to bind-mount `/var/run/docker.sock` into the
container so that Docker is available in the context of that image.

View file

@ -759,7 +759,7 @@ While you work on a test suite, you may want to run these specs in watch mode, s
# Watch and rerun all specs matching the name icon
yarn jest --watch icon
# Watch and rerun one specifc file
# Watch and rerun one specific file
yarn jest --watch path/to/spec/file.spec.js
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -123,6 +123,24 @@ latest versions of the scanning tools without having to do anything. There are s
with this approach, however, and there is a
[plan to resolve them](https://gitlab.com/gitlab-org/gitlab/-/issues/9725).
## Viewing security scan information in merge requests **(CORE)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4393) in GitLab Core 13.5.
> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/273205) in 13.6.
> - It's [deployed behind a feature flag](../feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It can be enabled or disabled for a single project.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-the-basic-security-widget). **(CORE ONLY)**
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
Merge requests which have run security scans let you know that the generated
reports are available to download.
![Security widget](img/security_widget_v13_6.png)
## Interacting with the vulnerabilities
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.8.
@ -594,3 +612,28 @@ Analyzer results are displayed in the [job logs](../../ci/pipelines/#expand-and-
[Merge Request widget](sast/index.md#overview)
or [Security Dashboard](security_dashboard/index.md).
There is [an open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/235772) in which changes to this behavior are being discussed.
### Enable or disable the basic security widget **(CORE ONLY)**
The basic security widget is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../feature_flags.md)
can opt to disable it.
To enable it:
```ruby
# For the instance
Feature.enable(:core_security_mr_widget)
# For a single project
Feature.enable(:core_security_mr_widget, Project.find(<project id>))
```
To disable it:
```ruby
# For the instance
Feature.disable(:core_security_mr_widget)
# For a single project
Feature.disable(:core_security_mr_widget, Project.find(<project id>))
```

View file

@ -77,8 +77,10 @@ neither stable nor versioned yet. For this reason, GitLab only guarantees compat
between corresponding major.minor (X.Y) versions of GitLab and its cluster side
component, `agentk`.
Upgrade your agent installations together with GitLab upgrades: if you install
GitLab version 13.6, use version 13.6.x versions of `agentk`.
Upgrade your agent installations together with GitLab upgrades. To decide which version of `agentk`to install follow:
1. Open the [GITLAB_KAS_VERSION](https://gitlab.com/gitlab-org/gitlab/-/blob/master/GITLAB_KAS_VERSION) file from the GitLab Repository, which contains the latest `agentk` version associated with the `master` branch.
1. Change the `master` branch and select the Git tag associated with your version. For instance, you could change it to GitLab [v13.5.3-ee release](https://gitlab.com/gitlab-org/gitlab/-/blob/v13.5.3-ee/GITLAB_KAS_VERSION)
The available `agentk` versions can be found in
[its container registry](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/container_registry/eyJuYW1lIjoiZ2l0bGFiLW9yZy9jbHVzdGVyLWludGVncmF0aW9uL2dpdGxhYi1hZ2VudC9hZ2VudGsiLCJ0YWdzX3BhdGgiOiIvZ2l0bGFiLW9yZy9jbHVzdGVyLWludGVncmF0aW9uL2dpdGxhYi1hZ2VudC9yZWdpc3RyeS9yZXBvc2l0b3J5LzEyMjMyMDUvdGFncz9mb3JtYXQ9anNvbiIsImlkIjoxMjIzMjA1LCJjbGVhbnVwX3BvbGljeV9zdGFydGVkX2F0IjpudWxsfQ==).

View file

@ -134,7 +134,7 @@ For a demo of the Okta SAML setup including SCIM, see [Demo: Okta Group SAML & S
Under Okta's **Single sign-on URL** field, check the option **Use this for Recipient URL and Destination URL**.
We recommend:
For NameID, the following settings are recommended; for SCIM, the following settings are required:
- **Application username** (NameID) set to **Custom** `user.getInternalProperty("id")`.
- **Name ID Format** set to **Persistent**.

View file

@ -121,8 +121,12 @@ Once synchronized, changing the field mapped to `id` and `externalId` may cause
### Okta configuration steps
The SAML application that was created during [Single sign-on](index.md#okta-setup-notes) setup for [Okta](https://developer.okta.com/docs/guides/build-sso-integration/saml2/overview/) now needs to be set up for SCIM.
Before proceeding, be sure to complete the [GitLab configuration](#gitlab-configuration) process.
Before you start this section, complete the [GitLab configuration](#gitlab-configuration) process.
Make sure that you've also set up a SAML application for [Okta](https://developer.okta.com/docs/guides/build-sso-integration/saml2/overview/),
as described in the [Okta setup notes](index.md#okta-setup-notes)
Make sure that the Okta setup matches our documentation exactly, especially the NameID
configuration. Otherwise, the Okta SCIM app may not work properly.
1. Sign in to Okta.
1. If you see an **Admin** button in the top right, click the button. This will

View file

@ -38,7 +38,7 @@ Learn more about using CI/CD to build:
- [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd)
- [NuGet packages](../nuget_repository/index.md#publish-a-nuget-package-by-using-cicd)
- [Conan packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd)
- [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages)
- [PyPI packages](../pypi_repository/index.md#publish-a-pypi-package-by-using-cicd)
- [Generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd)
If you use CI/CD to build a package, extended activity information is displayed

View file

@ -4,42 +4,28 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab PyPI Repository
# PyPI packages in the Package Registry
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208747) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3.
With the GitLab PyPI Repository, every project can have its own space to store PyPI packages.
Publish PyPI packages in your projects Package Registry. Then install the
packages whenever you need to use them as a dependency.
The GitLab PyPI Repository works with:
- [pip](https://pypi.org/project/pip/)
- [twine](https://pypi.org/project/twine/)
## Setting up your development environment
You need a recent version of [pip](https://pypi.org/project/pip/) and [twine](https://pypi.org/project/twine/).
## Enabling the PyPI Repository
NOTE: **Note:**
This option is available only if your GitLab administrator has
[enabled support for the Package Registry](../../../administration/packages/index.md).
After the PyPI Repository is enabled, it is available for all new projects
by default. To enable it for existing projects, or if you want to disable it:
1. Navigate to your project's **Settings > General > Visibility, project features, permissions**.
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
You should then be able to see the **Packages & Registries** section on the left sidebar.
## Getting started
## Build a PyPI package
This section covers creating a new example PyPI package to upload. This is a
quickstart to test out the **GitLab PyPI Registry**. If you already understand how
to build and publish your own packages, move on to the [next section](#adding-the-gitlab-pypi-repository-as-a-source).
quickstart to test out the **GitLab PyPI Registry**. If you already understand
how to build and publish your own packages, move on to the [next section](#authenticate-with-the-package-registry).
### Install pip and twine
You need a recent version of [pip](https://pypi.org/project/pip/) and [twine](https://pypi.org/project/twine/).
### Create a project
@ -97,8 +83,8 @@ Type "help", "copyright", "credits" or "license" for more information.
Hello from MyPyPiPackage
```
Once we've verified that the sample project is working as above, we can next
work on creating a package.
After we've verified that the sample project is working as previously described,
we can next work on creating a package.
### Create a package
@ -111,13 +97,13 @@ touch setup.py
This file contains all the information about our package. For more information
about this file, see [creating setup.py](https://packaging.python.org/tutorials/packaging-projects/#creating-setup-py).
GitLab identifies packages based on
Becaue GitLab identifies packages based on
[Python normalized names (PEP-503)](https://www.python.org/dev/peps/pep-0503/#normalized-names),
so ensure your package name meets these requirements.
See the [installation section](#install-packages) for more details.
ensure your package name meets these requirements. See the [installation section](#publish-a-pypi-package-by-using-cicd)
for more details.
For this guide, we don't need to extensively fill out this file, simply add the
below to your `setup.py`:
For this guide, we don't need to extensively fill out this file. Add the
following to your `setup.py`:
```python
import setuptools
@ -138,7 +124,7 @@ setuptools.setup(
)
```
Save the file, then execute the setup like so:
Save the file, and then execute the setup:
```shell
python3 setup.py sdist bdist_wheel
@ -157,18 +143,21 @@ And confirm your output matches the below:
mypypipackage-0.0.1-py3-none-any.whl mypypipackage-0.0.1.tar.gz
```
Our package is now all set up and ready to be uploaded to the **GitLab PyPI
Package Registry**. Before we do so, we next need to set up authentication.
The package is now all set up and is ready to be uploaded to the
_GitLab PyPI Package Registry_. Before we do so, we next need to set up
authentication.
## Adding the GitLab PyPI Repository as a source
## Authenticate with the Package Registry
### Authenticating with a personal access token
### Authenticate with a personal access token
You need the following:
- A personal access token. You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api` for repository authentication.
- A personal access token. You can generate a
[personal access token](../../../user/profile/personal_access_tokens.md)
with the scope set to `api` for repository authentication.
- A suitable name for your source.
- Your project ID which can be found on the home page of your project.
- Your project ID, which is found on the home page of your project.
Edit your `~/.pypirc` file and add the following:
@ -183,13 +172,15 @@ username = __token__
password = <your personal access token>
```
### Authenticating with a deploy token
### Authenticate with a deploy token
You need the following:
- A deploy token. You can generate a [deploy token](./../../project/deploy_tokens/index.md) with the `read_package_registry` and/or `write_package_registry` scopes for repository authentication.
- A deploy token. You can generate a [deploy token](./../../project/deploy_tokens/index.md)
with the `read_package_registry` or `write_package_registry` scopes for
repository authentication.
- A suitable name for your source.
- Your project ID which can be found on the home page of your project.
- Your project ID, which is found on the home page of your project.
Edit your `~/.pypirc` file and add the following:
@ -204,16 +195,18 @@ username = <deploy token username>
password = <deploy token>
```
## Uploading packages
## Publish a PyPI package
When uploading packages, note that:
When publishing packages, note that:
- The maximum allowed size is 50 Megabytes.
- You cannot upload the same version of a package multiple times. If you try, you receive the error `Validation failed: File name has already been taken`.
- You can't upload the same version of a package multiple times. If you try,
you'll receive the error `Validation failed: File name has already been taken`.
### Ensure your version string is valid
If your version string (for example, `0.0.1`) is invalid, it will be rejected. GitLab uses the following regex to validate the version string.
If your version string (for example, `0.0.1`) is invalid, it will be rejected.
GitLab uses the following regex to validate the version string.
```ruby
\A(?:
@ -227,13 +220,14 @@ If your version string (for example, `0.0.1`) is invalid, it will be rejected. G
)\z}xi
```
You can play around with the regex and try your version strings on [this regular expression editor](https://rubular.com/r/FKM6d07ouoDaFV).
You can experiment with the regex and try your version strings using this
[regular expression editor](https://rubular.com/r/FKM6d07ouoDaFV).
For more details about the regex used, please check the [documentation here](https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions))
For more details about the regex used, review this [documentation](https://www.python.org/dev/peps/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions).
### Upload packages with Twine
### Publish a PyPI package by using twine
If you were following the guide above, then the `MyPyPiPackage` package should
If you were following the steps on this page, the `MyPyPiPackage` package should
be ready to be uploaded. Run the following command:
```shell
@ -260,8 +254,8 @@ you can upload to the repository with the authentication inline:
TWINE_PASSWORD=<personal_access_token or deploy_token> TWINE_USERNAME=<username or deploy_token_username> python3 -m twine upload --repository-url https://gitlab.com/api/v4/projects/<project_id>/packages/pypi dist/*
```
If you did not follow the guide above, then you need to ensure your package
has been properly built and you [created a PyPI package with `setuptools`](https://packaging.python.org/tutorials/packaging-projects/).
If you didn't use the steps on this page, you need to ensure your package has
been properly built, and that you [created a PyPI package with `setuptools`](https://packaging.python.org/tutorials/packaging-projects/).
You can then upload your package using the following command:
@ -272,9 +266,42 @@ python -m twine upload --repository <source_name> dist/<package_file>
Where:
- `<package_file>` is your package filename, ending in `.tar.gz` or `.whl`.
- `<source_name>` is the [source name used during setup](#adding-the-gitlab-pypi-repository-as-a-source).
- `<source_name>` is the [source name used during setup](#authenticate-with-the-package-registry).
## Install packages
### Publish a PyPI package by using CI/CD
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202012) in GitLab 13.4.
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you
can use `CI_JOB_TOKEN` in place of the personal access token or deploy a token
in your commands.
For example:
```yaml
image: python:latest
run:
script:
- pip install twine
- python setup.py sdist bdist_wheel
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
```
You can also use `CI_JOB_TOKEN` in a `~/.pypirc` file that you check into GitLab:
```ini
[distutils]
index-servers =
gitlab
[gitlab]
repository = https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/pypi
username = gitlab-ci-token
password = ${env.CI_JOB_TOKEN}
```
## Install a PyPI package
Install the latest version of a package using the following command:
@ -310,35 +337,3 @@ GitLab looks for packages using
so the characters `-`, `_`, and `.` are all treated the same and repeated characters are removed.
A `pip install` request for `my.package` looks for packages that match any of
the three characters, such as `my-package`, `my_package`, and `my....package`.
## Using GitLab CI with PyPI packages
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202012) in GitLab 13.4.
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you can use
`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
For example:
```yaml
image: python:latest
run:
script:
- pip install twine
- python setup.py sdist bdist_wheel
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
```
You can also use `CI_JOB_TOKEN` in a `~/.pypirc` file that you check into GitLab:
```ini
[distutils]
index-servers =
gitlab
[gitlab]
repository = https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/pypi
username = gitlab-ci-token
password = ${env.CI_JOB_TOKEN}
```

View file

@ -403,6 +403,7 @@ module QA
module Layout
autoload :Banner, 'qa/page/layout/banner'
autoload :Flash, 'qa/page/layout/flash'
autoload :PerformanceBar, 'qa/page/layout/performance_bar'
end

View file

@ -13,11 +13,19 @@ module QA
base.view 'app/views/shared/_commit_message_container.html.haml' do
element :commit_message, "text_area_tag 'commit_message'" # rubocop:disable QA/ElementWithPattern
end
base.view 'app/views/projects/commits/_commit.html.haml' do
element :commit_content
end
end
def add_commit_message(message)
fill_in 'commit_message', with: message
end
def has_commit_message?(text)
has_element?(:commit_content, text: text)
end
end
end
end

View file

@ -7,16 +7,25 @@ module QA
include Shared::CommitMessage
include Project::SubMenus::Settings
include Project::SubMenus::Common
include Layout::Flash
view 'app/helpers/blob_helper.rb' do
element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern
element :delete_button, '_("Delete")' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/blob/_header_content.html.haml' do
element :file_name_content
end
view 'app/views/projects/blob/_remove.html.haml' do
element :delete_file_button, "button_tag 'Delete file'" # rubocop:disable QA/ElementWithPattern
end
view 'app/views/shared/_file_highlight.html.haml' do
element :file_content
end
def click_edit
click_on 'Edit'
end
@ -28,6 +37,18 @@ module QA
def click_delete_file
click_on 'Delete file'
end
def has_file?(name)
has_element?(:file_name_content, text: name)
end
def has_no_file?(name)
has_no_element?(:file_name_content, text: name)
end
def has_file_content?(text)
has_element?(:file_content, text: text)
end
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
module QA
module Page
module Layout
module Flash
extend QA::Page::PageConcern
def self.included(base)
super
base.view 'app/views/layouts/_flash.html.haml' do
element :flash_container
end
end
def has_notice?(message)
within_element(:flash_container) do
has_text?(message)
end
end
end
end
end
end

View file

@ -177,6 +177,10 @@ module QA
has_element?(:file_name_content, text: file_name)
end
def has_no_file?(file_name)
has_no_element?(:file_name_content, text: file_name)
end
def has_merge_button?
refresh

View file

@ -4,9 +4,11 @@ module QA
module Page
module Project
class Show < Page::Base
include Layout::Flash
include Page::Component::ClonePanel
include Page::Component::Breadcrumbs
include Page::Project::SubMenus::Settings
include Page::File::Shared::CommitMessage
view 'app/assets/javascripts/repository/components/preview/index.vue' do
element :blob_viewer_content
@ -121,6 +123,12 @@ module QA
end
end
def has_no_file?(name)
within_element(:file_tree_table) do
has_no_element?(:file_name_link, text: name)
end
end
def has_name?(name)
has_element?(:project_name_content, text: name)
end
@ -129,10 +137,6 @@ module QA
has_element?(:blob_viewer_content, text: text)
end
def last_commit_content
find_element(:commit_content).text
end
def new_merge_request
wait_until(reload: true) do
has_css?(element_selector_css(:create_merge_request))

View file

@ -13,6 +13,13 @@ module QA
attribute :project do
Project.fabricate! do |resource|
resource.name = 'project-with-new-file'
# Creating the first file via the Wed IDE is tested in
# browser_ui/3_create/web_ide/create_first_file_in_web_ide_spec.rb
# So here we want to use the old blob viewer, which is not
# available via the UI unless at least one file exists, which
# is why we create the project with a readme file.
resource.initialize_with_readme = true
end
end
@ -25,16 +32,13 @@ module QA
def fabricate!
project.visit!
Page::Project::Show.perform(&:create_first_new_file!)
Page::Project::Show.perform(&:create_new_file!)
Page::Project::WebIDE::Edit.perform do |ide|
ide.add_file(@name, @content)
ide.commit_changes(@commit_message)
ide.go_to_project
end
Page::Project::Show.perform do |project|
project.click_file(@name)
Page::File::Form.perform do |form|
form.add_name(@name)
form.add_content(@content)
form.add_commit_message(@commit_message)
form.commit_changes
end
end

View file

@ -1,11 +1,12 @@
# frozen_string_literal: true
module QA
include HaveFileMatcher
RSpec.describe 'Create' do
describe 'Files management' do
it 'user creates, edits and deletes a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/451' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
Flow::Login.sign_in
# Create
file_name = 'QA Test - File name'
@ -18,9 +19,13 @@ module QA
file.commit_message = commit_message_for_create
end
expect(page).to have_content(file_name)
expect(page).to have_content(file_content)
expect(page).to have_content(commit_message_for_create)
Page::File::Show.perform do |file|
aggregate_failures 'file details' do
expect(file).to have_file(file_name)
expect(file).to have_file_content(file_content)
expect(file).to have_commit_message(commit_message_for_create)
end
end
# Edit
updated_file_content = 'QA Test - Updated file content'
@ -28,29 +33,37 @@ module QA
Page::File::Show.perform(&:click_edit)
Page::File::Form.act do
remove_content
add_content(updated_file_content)
add_commit_message(commit_message_for_update)
commit_changes
Page::File::Form.perform do |file|
file.remove_content
file.add_content(updated_file_content)
file.add_commit_message(commit_message_for_update)
file.commit_changes
end
expect(page).to have_content('Your changes have been successfully committed.')
expect(page).to have_content(updated_file_content)
expect(page).to have_content(commit_message_for_update)
Page::File::Show.perform do |file|
aggregate_failures 'file details' do
expect(file).to have_notice('Your changes have been successfully committed.')
expect(file).to have_file_content(updated_file_content)
expect(file).to have_commit_message(commit_message_for_update)
end
end
# Delete
commit_message_for_delete = 'QA Test - Delete file'
Page::File::Show.act do
click_delete
add_commit_message(commit_message_for_delete)
click_delete_file
Page::File::Show.perform do |file|
file.click_delete
file.add_commit_message(commit_message_for_delete)
file.click_delete_file
end
expect(page).to have_content('The file has been successfully deleted.')
expect(page).to have_content(commit_message_for_delete)
expect(page).to have_no_content(file_name)
Page::Project::Show.perform do |project|
aggregate_failures 'file details' do
expect(project).to have_notice('The file has been successfully deleted.')
expect(project).to have_commit_message(commit_message_for_delete)
expect(project).not_to have_file(file_name)
end
end
end
end
end

View file

@ -14,6 +14,7 @@ QA::Runtime::Browser.configure!
QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) if QA::Runtime::Env.runtime_scenario_attributes
Dir[::File.join(__dir__, "support/helpers/*.rb")].sort.each { |f| require f }
Dir[::File.join(__dir__, "support/matchers/*.rb")].sort.each { |f| require f }
Dir[::File.join(__dir__, "support/shared_contexts/*.rb")].sort.each { |f| require f }
Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| require f }

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
module HaveFileMatcher
RSpec::Matchers.define :have_file do |file|
match do |page_object|
page_object.has_file?(file)
end
match_when_negated do |page_object|
page_object.has_no_file?(file)
end
end
end

View file

@ -52,20 +52,29 @@ RSpec.describe 'Dashboard Merge Requests' do
end
context 'merge requests exist' do
let_it_be(:author_user) { create(:user) }
let(:label) { create(:label) }
let!(:assigned_merge_request) do
create(:merge_request,
assignees: [current_user],
source_project: project,
author: create(:user))
author: author_user)
end
let!(:review_requested_merge_request) do
create(:merge_request,
reviewers: [current_user],
source_branch: 'review',
source_project: project,
author: author_user)
end
let!(:assigned_merge_request_from_fork) do
create(:merge_request,
source_branch: 'markdown', assignees: [current_user],
target_project: public_project, source_project: forked_project,
author: create(:user))
author: author_user)
end
let!(:authored_merge_request) do
@ -94,7 +103,7 @@ RSpec.describe 'Dashboard Merge Requests' do
create(:merge_request,
source_branch: 'fix',
source_project: project,
author: create(:user))
author: author_user)
end
before do
@ -111,6 +120,10 @@ RSpec.describe 'Dashboard Merge Requests' do
expect(page).not_to have_content(labeled_merge_request.title)
end
it 'shows review requested merge requests' do
expect(page).to have_content(review_requested_merge_request.title)
end
it 'shows authored merge requests', :js do
reset_filters
input_filtered_search("author:=#{current_user.to_reference}")

View file

@ -333,6 +333,8 @@ RSpec.describe MergeRequestsFinder do
end
context 'assignee filtering' do
let_it_be(:user3) { create(:user) }
let(:issuables) { described_class.new(user, params).execute }
it_behaves_like 'assignee ID filter' do
@ -351,7 +353,6 @@ RSpec.describe MergeRequestsFinder do
merge_request3.assignees = [user2, user3]
end
let_it_be(:user3) { create(:user) }
let(:params) { { assignee_username: [user2.username, user3.username] } }
let(:expected_issuables) { [merge_request3] }
end
@ -366,7 +367,6 @@ RSpec.describe MergeRequestsFinder do
end
it_behaves_like 'no assignee filter' do
let_it_be(:user3) { create(:user) }
let(:expected_issuables) { [merge_request4, merge_request5] }
end
@ -374,30 +374,54 @@ RSpec.describe MergeRequestsFinder do
let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
end
context 'filtering by group milestone' do
let(:group_milestone) { create(:milestone, group: group) }
context 'with just reviewers' do
it_behaves_like 'assignee username filter' do
before do
merge_request4.reviewers = [user3]
merge_request4.assignees = []
end
before do
merge_request1.update!(milestone: group_milestone)
merge_request2.update!(milestone: group_milestone)
let(:params) { { assignee_username: [user3.username] } }
let(:expected_issuables) { [merge_request4] }
end
end
it 'returns merge requests assigned to that group milestone' do
params = { milestone_title: group_milestone.title }
context 'with an additional reviewer' do
it_behaves_like 'assignee username filter' do
before do
merge_request3.assignees = [user3]
merge_request4.reviewers = [user3]
end
let(:params) { { assignee_username: [user3.username] } }
let(:expected_issuables) { [merge_request3, merge_request4] }
end
end
end
context 'filtering by group milestone' do
let(:group_milestone) { create(:milestone, group: group) }
before do
merge_request1.update!(milestone: group_milestone)
merge_request2.update!(milestone: group_milestone)
end
it 'returns merge requests assigned to that group milestone' do
params = { milestone_title: group_milestone.title }
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
end
context 'using NOT' do
let(:params) { { not: { milestone_title: group_milestone.title } } }
it 'returns MRs not assigned to that group milestone' do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
end
context 'using NOT' do
let(:params) { { not: { milestone_title: group_milestone.title } } }
it 'returns MRs not assigned to that group milestone' do
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
end
expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
end
end
end
@ -563,6 +587,27 @@ RSpec.describe MergeRequestsFinder do
expect(mrs).to eq([mr2])
end
end
it 'does not raise any exception with complex filters' do
# available filters from MergeRequest dashboard UI
params = {
project_id: project1.id,
scope: 'authored',
state: 'opened',
author_username: user.username,
assignee_username: user.username,
approver_usernames: [user.username],
approved_by_usernames: [user.username],
milestone_title: 'none',
release_tag: 'none',
label_names: 'none',
my_reaction_emoji: 'none',
draft: 'no'
}
merge_requests = described_class.new(user, params).execute
expect { merge_requests.load }.not_to raise_error
end
end
describe '#row_count', :request_store do

3
spec/fixtures/csv_no_headers.csv vendored Normal file
View file

@ -0,0 +1,3 @@
Issue in 中文,Test description
"Hello","World"
"Title with quote""",Description
1 Issue in 中文 Test description
2 Hello World
3 Title with quote" Description

View file

@ -5,126 +5,15 @@ require 'spec_helper'
RSpec.describe Issues::ImportCsvService do
let(:project) { create(:project) }
let(:user) { create(:user) }
subject do
let(:service) do
uploader = FileUploader.new(project)
uploader.store!(file)
described_class.new(user, project, uploader).execute
described_class.new(user, project, uploader)
end
shared_examples_for 'an issue importer' do
it 'records the import attempt' do
expect { subject }
.to change { Issues::CsvImport.where(project: project, user: user).count }
.by 1
end
end
describe '#execute' do
context 'invalid file' do
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
it 'returns invalid file error' do
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
expect(subject[:success]).to eq(0)
expect(subject[:parse_error]).to eq(true)
end
it_behaves_like 'an issue importer'
end
context 'with a file generated by Gitlab CSV export' do
let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') }
it 'imports the CSV without errors' do
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
expect(subject[:success]).to eq(4)
expect(subject[:error_lines]).to eq([])
expect(subject[:parse_error]).to eq(false)
end
it 'correctly sets the issue attributes' do
expect { subject }.to change { project.issues.count }.by 4
expect(project.issues.reload.last).to have_attributes(
title: 'Test Title',
description: 'Test Description'
)
end
it_behaves_like 'an issue importer'
end
context 'comma delimited file' do
let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') }
it 'imports CSV without errors' do
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
expect(subject[:success]).to eq(3)
expect(subject[:error_lines]).to eq([])
expect(subject[:parse_error]).to eq(false)
end
it 'correctly sets the issue attributes' do
expect { subject }.to change { project.issues.count }.by 3
expect(project.issues.reload.last).to have_attributes(
title: 'Title with quote"',
description: 'Description'
)
end
it_behaves_like 'an issue importer'
end
context 'tab delimited file with error row' do
let(:file) { fixture_file_upload('spec/fixtures/csv_tab.csv') }
it 'imports CSV with some error rows' do
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
expect(subject[:success]).to eq(2)
expect(subject[:error_lines]).to eq([3])
expect(subject[:parse_error]).to eq(false)
end
it 'correctly sets the issue attributes' do
expect { subject }.to change { project.issues.count }.by 2
expect(project.issues.reload.last).to have_attributes(
title: 'Hello',
description: 'World'
)
end
it_behaves_like 'an issue importer'
end
context 'semicolon delimited file with CRLF' do
let(:file) { fixture_file_upload('spec/fixtures/csv_semicolon.csv') }
it 'imports CSV with a blank row' do
expect(Notify).to receive_message_chain(:import_issues_csv_email, :deliver_later)
expect(subject[:success]).to eq(3)
expect(subject[:error_lines]).to eq([4])
expect(subject[:parse_error]).to eq(false)
end
it 'correctly sets the issue attributes' do
expect { subject }.to change { project.issues.count }.by 3
expect(project.issues.reload.last).to have_attributes(
title: 'Hello',
description: 'World'
)
end
it_behaves_like 'an issue importer'
end
include_examples 'issuable import csv service', 'issue' do
let(:issuables) { project.issues }
let(:email_method) { :import_issues_csv_email }
end
end

View file

@ -0,0 +1,144 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'issuable import csv service' do |issuable_type|
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:user) { create(:user) }
subject { service.execute }
shared_examples_for 'an issuable importer' do
if issuable_type == 'issue'
it 'records the import attempt if resource is an issue' do
expect { subject }
.to change { Issues::CsvImport.where(project: project, user: user).count }
.by 1
end
end
end
shared_examples_for 'importer with email notification' do
if issuable_type == 'issue'
it 'notifies user of import result' do
expect(Notify).to receive_message_chain(email_method, :deliver_later)
subject
end
end
end
describe '#execute' do
context 'invalid file' do
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
it 'returns invalid file error' do
expect(subject[:success]).to eq(0)
expect(subject[:parse_error]).to eq(true)
end
it_behaves_like 'importer with email notification'
it_behaves_like 'an issuable importer'
end
context 'file without headers' do
let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') }
it 'returns invalid file error' do
expect(subject[:success]).to eq(0)
expect(subject[:parse_error]).to eq(true)
end
it_behaves_like 'importer with email notification'
it_behaves_like 'an issuable importer'
end
context 'with a file generated by Gitlab CSV export' do
let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') }
it 'imports the CSV without errors' do
expect(subject[:success]).to eq(4)
expect(subject[:error_lines]).to eq([])
expect(subject[:parse_error]).to eq(false)
end
it 'correctly sets the issuable attributes' do
expect { subject }.to change { issuables.count }.by 4
expect(issuables.reload.last).to have_attributes(
title: 'Test Title',
description: 'Test Description'
)
end
it_behaves_like 'importer with email notification'
it_behaves_like 'an issuable importer'
end
context 'comma delimited file' do
let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') }
it 'imports CSV without errors' do
expect(subject[:success]).to eq(3)
expect(subject[:error_lines]).to eq([])
expect(subject[:parse_error]).to eq(false)
end
it 'correctly sets the issuable attributes' do
expect { subject }.to change { issuables.count }.by 3
expect(issuables.reload.last).to have_attributes(
title: 'Title with quote"',
description: 'Description'
)
end
it_behaves_like 'importer with email notification'
it_behaves_like 'an issuable importer'
end
context 'tab delimited file with error row' do
let(:file) { fixture_file_upload('spec/fixtures/csv_tab.csv') }
it 'imports CSV with some error rows' do
expect(subject[:success]).to eq(2)
expect(subject[:error_lines]).to eq([3])
expect(subject[:parse_error]).to eq(false)
end
it 'correctly sets the issuable attributes' do
expect { subject }.to change { issuables.count }.by 2
expect(issuables.reload.last).to have_attributes(
title: 'Hello',
description: 'World'
)
end
it_behaves_like 'importer with email notification'
it_behaves_like 'an issuable importer'
end
context 'semicolon delimited file with CRLF' do
let(:file) { fixture_file_upload('spec/fixtures/csv_semicolon.csv') }
it 'imports CSV with a blank row' do
expect(subject[:success]).to eq(3)
expect(subject[:error_lines]).to eq([4])
expect(subject[:parse_error]).to eq(false)
end
it 'correctly sets the issuable attributes' do
expect { subject }.to change { issuables.count }.by 3
expect(issuables.reload.last).to have_attributes(
title: 'Hello',
description: 'World'
)
end
it_behaves_like 'importer with email notification'
it_behaves_like 'an issuable importer'
end
end
end