提交 5693fb6b 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 f149549c
......@@ -63,7 +63,7 @@ gem 'attr_encrypted', '~> 3.1.0'
gem 'u2f', '~> 0.2.1'
# GitLab Pages
gem 'validates_hostname', '~> 1.0.6'
gem 'validates_hostname', '~> 1.0.10'
gem 'rubyzip', '~> 2.0.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.5'
......
......@@ -530,7 +530,7 @@ GEM
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.8.2)
i18n (1.8.3)
concurrent-ruby (~> 1.0)
i18n_data (0.8.0)
icalendar (2.4.1)
......@@ -1115,7 +1115,7 @@ GEM
validate_url (1.0.8)
activemodel (>= 3.0.0)
public_suffix
validates_hostname (1.0.6)
validates_hostname (1.0.10)
activerecord (>= 3.0)
activesupport (>= 3.0)
version_sorter (2.2.4)
......@@ -1401,7 +1401,7 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.4)
unleash (~> 0.1.5)
valid_email (~> 0.1)
validates_hostname (~> 1.0.6)
validates_hostname (~> 1.0.10)
version_sorter (~> 2.2.4)
vmstat (~> 2.3.0)
webmock (~> 3.5.1)
......
......@@ -221,7 +221,7 @@ export default {
</script>
<template>
<div class="vue-filtered-search-bar-container d-flex">
<div class="vue-filtered-search-bar-container d-md-flex">
<gl-filtered-search
v-model="filterValue"
:placeholder="searchInputPlaceholder"
......@@ -230,8 +230,8 @@ export default {
class="flex-grow-1"
@submit="handleFilterSubmit"
/>
<gl-button-group class="ml-2">
<gl-dropdown :text="selectedSortOption.title" :right="true">
<gl-button-group class="sort-dropdown-container d-flex">
<gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
<gl-dropdown-item
v-for="sortBy in sortOptions"
:key="sortBy.id"
......@@ -245,6 +245,7 @@ export default {
v-gl-tooltip
:title="sortDirectionTooltip"
:icon="sortDirectionIcon"
class="flex-shrink-1"
@click="handleSortDirectionClick"
/>
</gl-button-group>
......
......@@ -14,7 +14,6 @@ const TOOLBAR_ITEM_CONFIGS = [
{ isDivider: true },
{ icon: 'quote', command: 'Blockquote', tooltip: __('Insert a quote') },
{ icon: 'link', event: 'openPopupAddLink', tooltip: __('Add a link') },
{ icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
{ isDivider: true },
{ icon: 'list-bulleted', command: 'UL', tooltip: __('Add a bullet list') },
{ icon: 'list-numbered', command: 'OL', tooltip: __('Add a numbered list') },
......@@ -27,6 +26,7 @@ const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'doc-image', event: CUSTOM_EVENTS.openAddImageModal, tooltip: __('Insert an image') },
{ isDivider: true },
{ icon: 'code', command: 'Code', tooltip: __('Insert inline code') },
{ icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
];
export const EDITOR_OPTIONS = {
......
......@@ -449,3 +449,17 @@
font-size: 13px;
}
}
.vue-filtered-search-bar-container {
@include media-breakpoint-up(md) {
.sort-dropdown-container {
margin-left: 10px;
}
}
@include media-breakpoint-down(sm) {
.sort-dropdown-container {
margin-top: 10px;
}
}
}
# frozen_string_literal: true
module Mutations
module JiraImport
class ImportUsers < BaseMutation
include ResolvesProject
graphql_name 'JiraImportUsers'
field :jira_users,
[Types::JiraUserType],
null: true,
description: 'Users returned from Jira, matched by email and name if possible.'
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to import the Jira users into'
argument :start_at, GraphQL::INT_TYPE,
required: false,
description: 'The index of the record the import should started at, default 0 (50 records returned)'
def resolve(project_path:, start_at:)
project = authorized_find!(full_path: project_path)
service_response = ::JiraImport::UsersImporter.new(context[:current_user], project, start_at).execute
{
jira_users: service_response.payload,
errors: service_response.errors
}
end
private
def find_object(full_path:)
resolve_project(full_path: full_path)
end
def authorized_resource?(project)
Ability.allowed?(context[:current_user], :admin_project, project)
end
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
# Authorization is at project level for owners or admins on mutation level
class JiraUserType < BaseObject
graphql_name 'JiraUser'
field :jira_account_id, GraphQL::STRING_TYPE, null: false,
description: 'Account id of the Jira user'
field :jira_display_name, GraphQL::STRING_TYPE, null: false,
description: 'Display name of the Jira user'
field :jira_email, GraphQL::STRING_TYPE, null: true,
description: 'Email of the Jira user, returned only for users with public emails'
field :gitlab_id, GraphQL::INT_TYPE, null: true,
description: 'Id of the matched GitLab user'
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -48,6 +48,7 @@ module Types
mount_mutation Mutations::Snippets::Create
mount_mutation Mutations::Snippets::MarkAsSpam
mount_mutation Mutations::JiraImport::Start
mount_mutation Mutations::JiraImport::ImportUsers
mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
mount_mutation Mutations::ContainerExpirationPolicies::Update
......
......@@ -22,6 +22,8 @@ module JiraImport
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
Gitlab::ErrorTracking.track_exception(error, project_id: project.id, request: url)
ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}")
rescue Projects::ImportService::Error => error
ServiceResponse.error(message: error.message)
end
private
......
......@@ -26,6 +26,12 @@ class ServiceResponse
status == :error
end
def errors
return [] unless error?
Array.wrap(message)
end
private
attr_writer :status, :message, :http_status, :payload
......
---
title: Resolve incorrect x-axis padding on the Environments Dashboard
merge_request: 32533
author:
type: fixed
---
title: Create graphQL endpoint for Jira users import
merge_request: 33501
author:
type: added
---
title: Add route for the lost-and-found group and update the route of orphaned projects
merge_request: 33653
author:
type: fixed
---
title: Update Static Site Editor toolbar to group inline-code and code-block buttons together
merge_request: 34006
author:
type: changed
---
title: Update validates_hostname gem with support for more TLDs
merge_request: 34010
author:
type: fixed
# frozen_string_literal: true
# This migration adds or updates the routes for all the entities affected by
# post-migration '20200511083541_cleanup_projects_with_missing_namespace'
# - A route is added for the 'lost-and-found' group
# - A route is added for the Ghost user (if not already defined)
# - The routes for all the orphaned projects that were moved under the 'lost-and-found'
# group are updated to reflect the new path
class UpdateRoutesForLostAndFoundGroupAndOrphanedProjects < ActiveRecord::Migration[6.0]
DOWNTIME = false
class User < ActiveRecord::Base
self.table_name = 'users'
LOST_AND_FOUND_GROUP = 'lost-and-found'
USER_TYPE_GHOST = 5
ACCESS_LEVEL_OWNER = 50
has_one :namespace, -> { where(type: nil) },
foreign_key: :owner_id, inverse_of: :owner, autosave: true,
class_name: 'UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace'
def lost_and_found_group
# Find the 'lost-and-found' group
# There should only be one Group owned by the Ghost user starting with 'lost-and-found'
Group
.joins('INNER JOIN members ON namespaces.id = members.source_id')
.where('namespaces.type = ?', 'Group')
.where('members.type = ?', 'GroupMember')
.where('members.source_type = ?', 'Namespace')
.where('members.user_id = ?', self.id)
.where('members.access_level = ?', ACCESS_LEVEL_OWNER)
.find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
end
class << self
# Return the ghost user
def ghost
User.find_by(user_type: USER_TYPE_GHOST)
end
end
end
# Temporary Concern to not repeat the same methods twice
module HasPath
extend ActiveSupport::Concern
def full_path
if parent && path
parent.full_path + '/' + path
else
path
end
end
def full_name
if parent && name
parent.full_name + ' / ' + name
else
name
end
end
end
class Namespace < ActiveRecord::Base
include HasPath
self.table_name = 'namespaces'
belongs_to :owner, class_name: 'UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::User'
belongs_to :parent, class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
has_many :children, foreign_key: :parent_id,
class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
has_many :projects, class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Project"
def ensure_route!
unless Route.for_source('Namespace', self.id)
Route.create!(
source_id: self.id,
source_type: 'Namespace',
path: self.full_path,
name: self.full_name
)
end
end
end
class Group < Namespace
# Disable STI to allow us to manually set "type = 'Group'"
# Otherwise rails forces "type = CleanupProjectsWithMissingNamespace::Group"
self.inheritance_column = :_type_disabled
end
class Route < ActiveRecord::Base
self.table_name = 'routes'
def self.for_source(source_type, source_id)
Route.find_by(source_type: source_type, source_id: source_id)
end
end
class Project < ActiveRecord::Base
include HasPath
self.table_name = 'projects'
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id',
class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Group"
belongs_to :namespace,
class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
def ensure_route!
Route.find_or_initialize_by(source_type: 'Project', source_id: self.id).tap do |record|
record.path = self.full_path
record.name = self.full_name
record.save!
end
end
end
def up
# Reset the column information of all the models that update the database
# to ensure the Active Record's knowledge of the table structure is current
Namespace.reset_column_information
Route.reset_column_information
# Find the ghost user, its namespace and the "lost and found" group
ghost_user = User.ghost
return unless ghost_user # No reason to continue if there is no Ghost user
ghost_namespace = ghost_user.namespace
lost_and_found_group = ghost_user.lost_and_found_group
# No reason to continue if there is no 'lost-and-found' group
# 1. No orphaned projects were found in this instance, or
# 2. The 'lost-and-found' group and the orphaned projects have been already deleted
return unless lost_and_found_group
# Update the 'lost-and-found' group description to be more self-explanatory
lost_and_found_group.description =
'Group for storing projects that were not properly deleted. '\
'It should be considered as a system level Group with non-working '\
'projects inside it. The contents may be deleted with a future update. '\
'More info: gitlab.com/gitlab-org/gitlab/-/issues/198603'
lost_and_found_group.save!
# Update the routes for the Ghost user, the "lost and found" group
# and all the orphaned projects
ghost_namespace.ensure_route!
lost_and_found_group.ensure_route!
# The following does a fast index scan by namespace_id
# No reason to process in batches:
# - 66 projects in GitLab.com, less than 1ms execution time to fetch them
# with a constant update time for each
lost_and_found_group.projects.each do |project|
project.ensure_route!
end
end
def down
# no-op
end
end
......@@ -13797,6 +13797,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200528054112
20200528123703
20200528125905
20200602143020
20200603073101
\.
......@@ -5933,6 +5933,46 @@ type JiraImportStartPayload {
jiraImport: JiraImport
}
"""
Autogenerated input type of JiraImportUsers
"""
input JiraImportUsersInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The project to import the Jira users into
"""
projectPath: ID!
"""
The index of the record the import should started at, default 0 (50 records returned)
"""
startAt: Int
}
"""
Autogenerated return type of JiraImportUsers
"""
type JiraImportUsersPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
Users returned from Jira, matched by email and name if possible.
"""
jiraUsers: [JiraUser!]
}
type JiraProject {
"""
Key of the Jira project
......@@ -6027,6 +6067,28 @@ type JiraService implements Service {
type: String
}
type JiraUser {
"""
Id of the matched GitLab user
"""
gitlabId: Int
"""
Account id of the Jira user
"""
jiraAccountId: String!
"""
Display name of the Jira user
"""
jiraDisplayName: String!
"""
Email of the Jira user, returned only for users with public emails
"""
jiraEmail: String
}
type Label {
"""
Background color of the label
......@@ -7271,6 +7333,7 @@ type Mutation {
issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
jiraImportUsers(input: JiraImportUsersInput!): JiraImportUsersPayload
markAsSpamSnippet(input: MarkAsSpamSnippetInput!): MarkAsSpamSnippetPayload
mergeRequestCreate(input: MergeRequestCreateInput!): MergeRequestCreatePayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
......
......@@ -16358,6 +16358,126 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "JiraImportUsersInput",
"description": "Autogenerated input type of JiraImportUsers",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project to import the Jira users into",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "startAt",
"description": "The index of the record the import should started at, default 0 (50 records returned)",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "JiraImportUsersPayload",
"description": "Autogenerated return type of JiraImportUsers",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraUsers",
"description": "Users returned from Jira, matched by email and name if possible.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "JiraUser",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "JiraProject",
......@@ -16641,6 +16761,83 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "JiraUser",
"description": null,
"fields": [
{
"name": "gitlabId",
"description": "Id of the matched GitLab user",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraAccountId",
"description": "Account id of the Jira user",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraDisplayName",
"description": "Display name of the Jira user",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraEmail",
"description": "Email of the Jira user, returned only for users with public emails",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Label",
......@@ -21053,6 +21250,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "jiraImportUsers",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "JiraImportUsersInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "JiraImportUsersPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "markAsSpamSnippet",
"description": null,
......@@ -869,6 +869,16 @@ Autogenerated return type of JiraImportStart
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `jiraImport` | JiraImport | The Jira import data after mutation |
## JiraImportUsersPayload
Autogenerated return type of JiraImportUsers
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `jiraUsers` | JiraUser! => Array | Users returned from Jira, matched by email and name if possible. |
## JiraProject
| Name | Type | Description |
......@@ -885,6 +895,15 @@ Autogenerated return type of JiraImportStart
| `projects` | JiraProjectConnection | List of Jira projects fetched through Jira REST API |
| `type` | String | Class name of the service |
## JiraUser
| Name | Type | Description |
| --- | ---- | ---------- |
| `gitlabId` | Int | Id of the matched GitLab user |
| `jiraAccountId` | String! | Account id of the Jira user |
| `jiraDisplayName` | String! | Display name of the Jira user |
| `jiraEmail` | String | Email of the Jira user, returned only for users with public emails |
## Label
| Name | Type | Description |
......
......@@ -109,7 +109,7 @@ are very appreciative of the work done by translators and proofreaders!
- Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [CrowdIn](https://crowdin.com/profile/wheleph)
- Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [CrowdIn](https://crowdin.com/profile/andruwa13)
- Welsh
- Proofreaders needed.
- Delyth Prys - [GitLab](https://gitlab.com/Delyth), [CrowdIn](https://crowdin.com/profile/DelythPrys)
<!-- vale gitlab.Spelling = YES -->
## Become a proofreader
......
......@@ -528,13 +528,12 @@ the group regardless of the IP restriction.
#### Allowed domain restriction **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
>- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7297) in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2.
>- Support for specifying multiple email domains [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33143) in GitLab 13.1
You can restrict access to groups by
allowing only users with email addresses in particular domains to be added to the group.
You can restrict access to groups by allowing only users with email addresses in particular domains to be added to the group.
Add email domains you want to allow and users with emails from different
domains won't be allowed to be added to this group.
Add email domains you want to allow and users with emails from different domains won't be allowed to be added to this group.
Some domains cannot be restricted. These are the most popular public email domains, such as:
......@@ -552,7 +551,7 @@ Some domains cannot be restricted. These are the most popular public email domai
To enable this feature:
1. Navigate to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section, and enter the domain name into **Restrict membership by email** field.
1. Expand the **Permissions, LFS, 2FA** section, and enter the domain names into **Restrict membership by email** field. You can enter multiple domains by separating each domain with a comma (,).
1. Click **Save changes**.
This will enable the domain-checking for all new users added to the group from this moment on.
......
......@@ -8468,7 +8468,7 @@ msgstr ""
msgid "EnvironmentsDashboard|More actions"
msgstr ""
msgid "EnvironmentsDashboard|Read more."
msgid "EnvironmentsDashboard|More information"
msgstr ""
msgid "EnvironmentsDashboard|Remove"
......@@ -9050,6 +9050,9 @@ msgstr ""
msgid "Exactly one of %{attributes} is required"
msgstr ""
msgid "Example: <code>acme.com,acme.co.in,acme.uk</code>."
msgstr ""
msgid "Example: @sub\\.company\\.com$"
msgstr ""
......@@ -11219,6 +11222,9 @@ msgstr ""
msgid "GroupSettings|You will need to update your local repositories to point to the new location."
msgstr ""
msgid "GroupSettings|cannot be changed by you"
msgstr ""
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
msgstr ""
......@@ -14346,6 +14352,9 @@ msgstr ""
msgid "MrDeploymentActions|Stop environment"
msgstr ""
msgid "Multiple domains are supported with comma delimiters."
msgstr ""
msgid "Multiple issue boards"
msgstr ""
......@@ -15208,7 +15217,7 @@ msgstr ""
msgid "Only project members will be imported. Group members will be skipped."
msgstr ""
msgid "Only users with an email address in this domain can be added to the group.<br>Example: <code>gitlab.com</code>. Some common domains are not allowed. %{read_more_link}."
msgid "Only verified users with an email address in any of these domains can be added to the group."
msgstr ""
msgid "Only ‘Reporter’ roles and above on tiers Premium / Silver and above can see Productivity Analytics."
......@@ -20470,6 +20479,9 @@ msgstr ""
msgid "Some child epics may be hidden due to applied filters"
msgstr ""
msgid "Some common domains are not allowed. %{read_more_link}."
msgstr ""
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
......@@ -26363,7 +26375,7 @@ msgstr ""
msgid "element is not a hierarchy"
msgstr ""
msgid "email '%{email}' does not match the allowed domain of '%{email_domain}'"
msgid "email '%{email}' does not match the allowed domains of %{email_domains}"
msgstr ""
msgid "email '%{email}' is not a verified email."
......
......@@ -5,10 +5,6 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200511080113_add_projects_foreign_key_to_namespaces.rb')
require Rails.root.join('db', 'post_migrate', '20200511083541_cleanup_projects_with_missing_namespace.rb')
LOST_AND_FOUND_GROUP = 'lost-and-found'
USER_TYPE_GHOST = 5
ACCESS_LEVEL_OWNER = 50
# In order to test the CleanupProjectsWithMissingNamespace migration, we need
# to first create an orphaned project (one with an invalid namespace_id)
# and then run the migration to check that the project was properly cleaned up
......@@ -77,31 +73,39 @@ describe CleanupProjectsWithMissingNamespace, :migration, schema: SchemaVersionF
end
it 'creates the ghost user' do
expect(users.where(user_type: USER_TYPE_GHOST).count).to eq(0)
expect(users.where(user_type: described_class::User::USER_TYPE_GHOST).count).to eq(0)
disable_migrations_output { migrate! }
expect(users.where(user_type: USER_TYPE_GHOST).count).to eq(1)
expect(users.where(user_type: described_class::User::USER_TYPE_GHOST).count).to eq(1)
end
it 'creates the lost-and-found group, owned by the ghost user' do
expect(
Group.where(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%")).count
described_class::Group.where(
described_class::Group
.arel_table[:name]
.matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
).count
).to eq(0)
disable_migrations_output { migrate! }
ghost_user = users.find_by(user_type: USER_TYPE_GHOST)
ghost_user = users.find_by(user_type: described_class::User::USER_TYPE_GHOST)
expect(
Group
described_class::Group
.joins('INNER JOIN members ON namespaces.id = members.source_id')
.where('namespaces.type = ?', 'Group')
.where('members.type = ?', 'GroupMember')
.where('members.source_type = ?', 'Namespace')
.where('members.user_id = ?', ghost_user.id)
.where('members.requested_at IS NULL')
.where('members.access_level = ?', ACCESS_LEVEL_OWNER)
.where(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
.where('members.access_level = ?', described_class::ACCESS_LEVEL_OWNER)
.where(
described_class::Group
.arel_table[:name]
.matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
)
.count
).to eq(1)
end
......@@ -114,7 +118,11 @@ describe CleanupProjectsWithMissingNamespace, :migration, schema: SchemaVersionF
disable_migrations_output { migrate! }
lost_and_found_group = Group.find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
lost_and_found_group = described_class::Group.find_by(
described_class::Group
.arel_table[:name]
.matches("#{described_class::User::LOST_AND_FOUND_GROUP}%")
)
orphaned_project = projects.find_by(id: orphaned_project.id)
expect(orphaned_project.visibility_level).to eq(0)
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200602143020_update_routes_for_lost_and_found_group_and_orphaned_projects.rb')
describe UpdateRoutesForLostAndFoundGroupAndOrphanedProjects, :migration do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:members) { table(:members) }
let(:projects) { table(:projects) }
let(:routes) { table(:routes) }
before do
# Create a Ghost User and its namnespace, but skip the route
ghost_user = users.create!(
name: 'Ghost User',
username: 'ghost',
email: 'ghost@example.com',
user_type: described_class::User::USER_TYPE_GHOST,
projects_limit: 100,
state: :active,
bio: 'This is a "Ghost User"'
)
namespaces.create!(
name: 'Ghost User',
path: 'ghost',
owner_id: ghost_user.id,
visibility_level: 20
)
# Create the 'lost-and-found', owned by the Ghost user, but with no route
lost_and_found_group = namespaces.create!(
name: described_class::User::LOST_AND_FOUND_GROUP,
path: described_class::User::LOST_AND_FOUND_GROUP,
type: 'Group',
description: 'Group to store orphaned projects',
visibility_level: 0
)
members.create!(
type: 'GroupMember',
source_id: lost_and_found_group.id,
user_id: ghost_user.id,
source_type: 'Namespace',
access_level: described_class::User::ACCESS_LEVEL_OWNER,
notification_level: 3
)
# Add an orphaned project under 'lost-and-found' but with the wrong path in its route
orphaned_project = projects.create!(
name: 'orphaned_project',
path: 'orphaned_project',
visibility_level: 20,
archived: false,
namespace_id: lost_and_found_group.id
)
routes.create!(
source_id: orphaned_project.id,
source_type: 'Project',
path: 'orphaned_project',
name: 'orphaned_project',
created_at: Time.zone.now,
updated_at: Time.zone.now
)
end
it 'creates the route for the ghost user namespace' do
expect(routes.where(path: 'ghost').count).to eq(0)
disable_migrations_output { migrate! }
expect(routes.where(path: 'ghost').count).to eq(1)
end
it 'creates the route for the lost-and-found group' do
expect(routes.where(path: described_class::User::LOST_AND_FOUND_GROUP).count).to eq(0)
disable_migrations_output { migrate! }
expect(routes.where(path: described_class::User::LOST_AND_FOUND_GROUP).count).to eq(1)
end
it 'updates the route for the orphaned project' do
orphaned_project_route = routes.find_by(path: 'orphaned_project')
expect(orphaned_project_route.name).to eq('orphaned_project')
disable_migrations_output { migrate! }
updated_route = routes.find_by(id: orphaned_project_route.id)
expect(updated_route.path).to eq("#{described_class::User::LOST_AND_FOUND_GROUP}/orphaned_project")
expect(updated_route.name).to eq("#{described_class::User::LOST_AND_FOUND_GROUP} / orphaned_project")
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Importing Jira Users' do
include JiraServiceHelper
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:project_path) { project.full_path }
let(:start_at) { 7 }
let(:mutation) do
variables = {
start_at: start_at,
project_path: project_path
}
graphql_mutation(:jira_import_users, variables)
end
def mutation_response
graphql_mutation_response(:jira_import_users)
end
def jira_import
mutation_response['jiraUsers']
end
context 'with anonymous user' do
let(:current_user) { nil }
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
context 'with user without permissions' do
let(:current_user) { user }
before do
project.add_developer(current_user)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
context 'when the user has permissions' do
let(:current_user) { user }
before do
project.add_maintainer(current_user)
end
context 'when the project path is invalid' do
let(:project_path) { 'foobar' }
it 'returns an an error' do
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
end
end
context 'when all params and permissions are ok' do
let(:importer) { instance_double(JiraImport::UsersImporter) }
before do
expect(JiraImport::UsersImporter).to receive(:new).with(current_user, project, 7)
.and_return(importer)
end
context 'when service returns a successful response' do
it 'returns imported users' do
users = [{ jira_account_id: '12a', jira_display_name: 'user 1' }]
result = ServiceResponse.success(payload: users)
expect(importer).to receive(:execute).and_return(result)
post_graphql_mutation(mutation, current_user: current_user)
expect(jira_import.length).to eq(1)
expect(jira_import.first['jiraAccountId']).to eq('12a')
expect(jira_import.first['jiraDisplayName']).to eq('user 1')
end
end
context 'when service returns an error response' do
it 'returns an error messaege' do
result = ServiceResponse.error(message: 'Some error')
expect(importer).to receive(:execute).and_return(result)
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to eq(['Some error'])
end
end
end
end
end
......@@ -20,8 +20,8 @@ describe JiraImport::UsersImporter do
end
context 'when Jira import is not configured properly' do
it 'raises an error' do
expect { subject }.to raise_error(Projects::ImportService::Error)
it 'returns an error' do
expect(subject.errors).to eq(['Jira integration not configured.'])
end
end
......
......@@ -84,4 +84,14 @@ describe ServiceResponse do
expect(described_class.error(message: 'Bad apple').error?).to eq(true)
end
end
describe '#errors' do
it 'returns an empty array for a successful response' do
expect(described_class.success.errors).to be_empty
end
it 'returns an array with a correct message for an error response' do
expect(described_class.error(message: 'error message').errors).to eq(['error message'])
end
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册