Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-02 15:08:52 +00:00
parent c80b69a93f
commit 215cb09934
39 changed files with 680 additions and 153 deletions

View File

@ -34,7 +34,9 @@ If applicable, any groups/projects that are happy to have this feature turned on
- [ ] Test on staging - [ ] Test on staging
- [ ] Ensure that documentation has been updated - [ ] Ensure that documentation has been updated
- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`) - [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`)
- [ ] Coordinate a time to enable the flag with `#production` and `#g_delivery` on slack. - [ ] Coordinate a time to enable the flag with the SRE oncall and release managers
- In `#production` by pinging `@sre-oncall`
- In `#g_delivery` by pinging `@release-managers`
- [ ] Announce on the issue an estimated time this will be enabled on GitLab.com - [ ] Announce on the issue an estimated time this will be enabled on GitLab.com
- [ ] Enable on GitLab.com by running chatops command in `#production` (`/chatops run feature set feature_name true`) - [ ] Enable on GitLab.com by running chatops command in `#production` (`/chatops run feature set feature_name true`)
- [ ] Cross post chatops Slack command to `#support_gitlab-com` ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#where-to-run-commands)) and in your team channel - [ ] Cross post chatops Slack command to `#support_gitlab-com` ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#where-to-run-commands)) and in your team channel
@ -42,4 +44,12 @@ If applicable, any groups/projects that are happy to have this feature turned on
- [ ] Remove feature flag and add changelog entry - [ ] Remove feature flag and add changelog entry
- [ ] After the flag removal is deployed, [clean up the feature flag](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) by running chatops command in `#production` channel - [ ] After the flag removal is deployed, [clean up the feature flag](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) by running chatops command in `#production` channel
## Rollback Steps
- [ ] This feature can be disabled by running the following Chatops command:
```
/chatops run feature set --project=gitlab-org/gitlab feature_name false
```
/label ~"feature flag" /label ~"feature flag"

View File

@ -8,7 +8,6 @@
<!-- What is the user problem you are trying to solve with this issue? --> <!-- What is the user problem you are trying to solve with this issue? -->
### Proposal ### Proposal
<!-- Use this section to explain the feature and how it will work. It can be helpful to add technical details, design proposals, and links to related epics or issues. --> <!-- Use this section to explain the feature and how it will work. It can be helpful to add technical details, design proposals, and links to related epics or issues. -->
@ -46,14 +45,14 @@ Personas are described at https://about.gitlab.com/handbook/marketing/product-ma
### User experience goal ### User experience goal
What is the single user experience workflow this problem addresses? What is the single user experience workflow this problem addresses?
For example, "The user should be able to use the UI/API/.gitlab-ci.yml with GitLab to <perform a specific task>" For example, "The user should be able to use the UI/API/.gitlab-ci.yml with GitLab to <perform a specific task>"
https://about.gitlab.com/handbook/engineering/ux/ux-research-training/user-story-mapping/ https://about.gitlab.com/handbook/engineering/ux/ux-research-training/user-story-mapping/
### Further details ### Further details
nclude use cases, benefits, goals, or any other details that will help us understand the problem better. Include use cases, benefits, goals, or any other details that will help us understand the problem better.
### Permissions and Security ### Permissions and Security
@ -75,7 +74,7 @@ Consider adding checkboxes and expectations of users with certain levels of memb
### Availability & Testing ### Availability & Testing
his section needs to be retained and filled in during the workflow planning breakdown phase of this feature proposal, if not earlier. This section needs to be retained and filled in during the workflow planning breakdown phase of this feature proposal, if not earlier.
What risks does this change pose to our availability? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? What risks does this change pose to our availability? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing?
@ -98,6 +97,3 @@ In which enterprise tier should this feature go? See https://about.gitlab.com/ha
### Is this a cross-stage feature? ### Is this a cross-stage feature?
Communicate if this change will affect multiple Stage Groups or product areas. We recommend always start with the assumption that a feature request will have an impact into another Group. Loop in the most relevant PM and Product Designer from that Group to provide strategic support to help align the Group's broader plan and vision, as well as to avoid UX and technical debt. https://about.gitlab.com/handbook/product/#cross-stage-features --> Communicate if this change will affect multiple Stage Groups or product areas. We recommend always start with the assumption that a feature request will have an impact into another Group. Loop in the most relevant PM and Product Designer from that Group to provide strategic support to help align the Group's broader plan and vision, as well as to avoid UX and technical debt. https://about.gitlab.com/handbook/product/#cross-stage-features -->

View File

@ -5,6 +5,7 @@ import InstanceStatisticsCountChart from './instance_statistics_count_chart.vue'
import UsersChart from './users_chart.vue'; import UsersChart from './users_chart.vue';
import pipelinesStatsQuery from '../graphql/queries/pipeline_stats.query.graphql'; import pipelinesStatsQuery from '../graphql/queries/pipeline_stats.query.graphql';
import issuesAndMergeRequestsQuery from '../graphql/queries/issues_and_merge_requests.query.graphql'; import issuesAndMergeRequestsQuery from '../graphql/queries/issues_and_merge_requests.query.graphql';
import ProjectsAndGroupsChart from './projects_and_groups_chart.vue';
import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants'; import { TODAY, TOTAL_DAYS_TO_SHOW, START_DATE } from '../constants';
const PIPELINES_KEY_TO_NAME_MAP = { const PIPELINES_KEY_TO_NAME_MAP = {
@ -32,6 +33,7 @@ export default {
InstanceCounts, InstanceCounts,
InstanceStatisticsCountChart, InstanceStatisticsCountChart,
UsersChart, UsersChart,
ProjectsAndGroupsChart,
}, },
TOTAL_DAYS_TO_SHOW, TOTAL_DAYS_TO_SHOW,
START_DATE, START_DATE,
@ -69,6 +71,11 @@ export default {
:end-date="$options.TODAY" :end-date="$options.TODAY"
:total-data-points="$options.TOTAL_DAYS_TO_SHOW" :total-data-points="$options.TOTAL_DAYS_TO_SHOW"
/> />
<projects-and-groups-chart
:start-date="$options.START_DATE"
:end-date="$options.TODAY"
:total-data-points="$options.TOTAL_DAYS_TO_SHOW"
/>
<instance-statistics-count-chart <instance-statistics-count-chart
v-for="chartOptions in $options.configs" v-for="chartOptions in $options.configs"
:key="chartOptions.chartTitle" :key="chartOptions.chartTitle"

View File

@ -0,0 +1,224 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import produce from 'immer';
import { sortBy } from 'lodash';
import * as Sentry from '~/sentry/wrapper';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import { s__, __ } from '~/locale';
import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
import latestGroupsQuery from '../graphql/queries/groups.query.graphql';
import latestProjectsQuery from '../graphql/queries/projects.query.graphql';
import { getAverageByMonth } from '../utils';
const sortByDate = data => sortBy(data, item => new Date(item[0]).getTime());
const averageAndSortData = (data = [], maxDataPoints) => {
const averaged = getAverageByMonth(
data.length > maxDataPoints ? data.slice(0, maxDataPoints) : data,
{ shouldRound: true },
);
return sortByDate(averaged);
};
export default {
name: 'ProjectsAndGroupsChart',
components: { GlAlert, GlLineChart, ChartSkeletonLoader },
props: {
startDate: {
type: Date,
required: true,
},
endDate: {
type: Date,
required: true,
},
totalDataPoints: {
type: Number,
required: true,
},
},
data() {
return {
loadingError: false,
errorMessage: '',
groups: [],
projects: [],
groupsPageInfo: null,
projectsPageInfo: null,
};
},
apollo: {
groups: {
query: latestGroupsQuery,
variables() {
return {
first: this.totalDataPoints,
after: null,
};
},
update(data) {
return data.groups?.nodes || [];
},
result({ data }) {
const {
groups: { pageInfo },
} = data;
this.groupsPageInfo = pageInfo;
this.fetchNextPage({
query: this.$apollo.queries.groups,
pageInfo: this.groupsPageInfo,
dataKey: 'groups',
errorMessage: this.$options.i18n.loadGroupsDataError,
});
},
error(error) {
this.handleError({
message: this.$options.i18n.loadGroupsDataError,
error,
dataKey: 'groups',
});
},
},
projects: {
query: latestProjectsQuery,
variables() {
return {
first: this.totalDataPoints,
after: null,
};
},
update(data) {
return data.projects?.nodes || [];
},
result({ data }) {
const {
projects: { pageInfo },
} = data;
this.projectsPageInfo = pageInfo;
this.fetchNextPage({
query: this.$apollo.queries.projects,
pageInfo: this.projectsPageInfo,
dataKey: 'projects',
errorMessage: this.$options.i18n.loadProjectsDataError,
});
},
error(error) {
this.handleError({
message: this.$options.i18n.loadProjectsDataError,
error,
dataKey: 'projects',
});
},
},
},
i18n: {
yAxisTitle: s__('InstanceStatistics|Total projects & groups'),
xAxisTitle: __('Month'),
loadChartError: s__(
'InstanceStatistics|Could not load the projects and groups chart. Please refresh the page to try again.',
),
loadProjectsDataError: s__('InstanceStatistics|There was an error while loading the projects'),
loadGroupsDataError: s__('InstanceStatistics|There was an error while loading the groups'),
noDataMessage: s__('InstanceStatistics|No data available.'),
},
computed: {
isLoadingGroups() {
return this.$apollo.queries.groups.loading || this.groupsPageInfo?.hasNextPage;
},
isLoadingProjects() {
return this.$apollo.queries.projects.loading || this.projectsPageInfo?.hasNextPage;
},
isLoading() {
return this.isLoadingProjects && this.isLoadingGroups;
},
groupChartData() {
return averageAndSortData(this.groups, this.totalDataPoints);
},
projectChartData() {
return averageAndSortData(this.projects, this.totalDataPoints);
},
hasNoData() {
const { projectChartData, groupChartData } = this;
return Boolean(!projectChartData.length && !groupChartData.length);
},
options() {
return {
xAxis: {
name: this.$options.i18n.xAxisTitle,
type: 'category',
axisLabel: {
formatter: value => {
return formatDateAsMonth(value);
},
},
},
yAxis: {
name: this.$options.i18n.yAxisTitle,
},
};
},
chartData() {
return [
{
name: s__('InstanceStatistics|Total projects'),
data: this.projectChartData,
},
{
name: s__('InstanceStatistics|Total groups'),
data: this.groupChartData,
},
];
},
},
methods: {
handleError({ error, message = this.$options.i18n.loadChartError, dataKey = null }) {
this.loadingError = true;
this.errorMessage = message;
if (!dataKey) {
this.projects = [];
this.groups = [];
} else {
this[dataKey] = [];
}
Sentry.captureException(error);
},
fetchNextPage({ pageInfo, query, dataKey, errorMessage }) {
if (pageInfo?.hasNextPage) {
query
.fetchMore({
variables: { first: this.totalDataPoints, after: pageInfo.endCursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
const results = produce(fetchMoreResult, newData => {
// eslint-disable-next-line no-param-reassign
newData[dataKey].nodes = [
...previousResult[dataKey].nodes,
...newData[dataKey].nodes,
];
});
return results;
},
})
.catch(error => {
this.handleError({ error, message: errorMessage, dataKey });
});
}
},
},
};
</script>
<template>
<div>
<h3>{{ $options.i18n.yAxisTitle }}</h3>
<chart-skeleton-loader v-if="isLoading" />
<gl-alert v-else-if="hasNoData" variant="info" :dismissible="false" class="gl-mt-3">
{{ $options.i18n.noDataMessage }}
</gl-alert>
<div v-else>
<gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">{{
errorMessage
}}</gl-alert>
<gl-line-chart :option="options" :include-legend-avg-max="true" :data="chartData" />
</div>
</div>
</template>

View File

@ -0,0 +1,13 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/count.fragment.graphql"
query getGroupsCount($first: Int, $after: String) {
groups: instanceStatisticsMeasurements(identifier: GROUPS, first: $first, after: $after) {
nodes {
...Count
}
pageInfo {
...PageInfo
}
}
}

View File

@ -0,0 +1,13 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/count.fragment.graphql"
query getProjectsCount($first: Int, $after: String) {
projects: instanceStatisticsMeasurements(identifier: PROJECTS, first: $first, after: $after) {
nodes {
...Count
}
pageInfo {
...PageInfo
}
}
}

View File

@ -6,8 +6,8 @@ import {
GlModal, GlModal,
GlAlert, GlAlert,
GlLoadingIcon, GlLoadingIcon,
GlDeprecatedDropdown, GlDropdown,
GlDeprecatedDropdownItem, GlDropdownItem,
GlButton, GlButton,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
@ -28,8 +28,8 @@ export default {
GlModal, GlModal,
GlAlert, GlAlert,
GlLoadingIcon, GlLoadingIcon,
GlDeprecatedDropdown, GlDropdown,
GlDeprecatedDropdownItem, GlDropdownItem,
TimeAgoTooltip, TimeAgoTooltip,
GlButton, GlButton,
}, },
@ -231,17 +231,17 @@ export default {
</template> </template>
</div> </div>
<div class="d-block d-sm-none dropdown"> <div class="d-block d-sm-none dropdown">
<gl-deprecated-dropdown :text="__('Options')" class="w-100" toggle-class="text-center"> <gl-dropdown :text="__('Options')" block>
<gl-deprecated-dropdown-item <gl-dropdown-item
v-for="(action, index) in personalSnippetActions" v-for="(action, index) in personalSnippetActions"
:key="index" :key="index"
:disabled="action.disabled" :disabled="action.disabled"
:title="action.title" :title="action.title"
:href="action.href" :href="action.href"
@click="action.click ? action.click() : undefined" @click="action.click ? action.click() : undefined"
>{{ action.text }}</gl-deprecated-dropdown-item >{{ action.text }}</gl-dropdown-item
> >
</gl-deprecated-dropdown> </gl-dropdown>
</div> </div>
</div> </div>

View File

@ -119,7 +119,7 @@ class Namespace < ApplicationRecord
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query, include_parents: false) def search(query, include_parents: false)
if include_parents if include_parents
where(id: Route.fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id)) where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id))
else else
fuzzy_search(query, [:path, :name]) fuzzy_search(query, [:path, :name])
end end

View File

@ -20,6 +20,7 @@ class Route < ApplicationRecord
scope :inside_path, -> (path) { where('routes.path LIKE ?', "#{sanitize_sql_like(path)}/%") } scope :inside_path, -> (path) { where('routes.path LIKE ?', "#{sanitize_sql_like(path)}/%") }
scope :for_routable, -> (routable) { where(source: routable) } scope :for_routable, -> (routable) { where(source: routable) }
scope :for_routable_type, -> (routable_type) { where(source_type: routable_type) }
scope :sort_by_path_length, -> { order('LENGTH(routes.path)', :path) } scope :sort_by_path_length, -> { order('LENGTH(routes.path)', :path) }
def rename_descendants def rename_descendants

View File

@ -0,0 +1,5 @@
---
title: Fix problems with Groups API search query parameter
merge_request: 46394
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add minimal access users to group members api endpoints
merge_request: 46238
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/snippets/components/snippet_header.vue
merge_request: 41428
author: nuwe1
type: other

View File

@ -18,7 +18,6 @@
- authentication_and_authorization - authentication_and_authorization
- auto_devops - auto_devops
- backup_restore - backup_restore
- behavior_analytics
- boards - boards
- chatops - chatops
- cloud_native_installation - cloud_native_installation
@ -56,7 +55,6 @@
- gitaly - gitaly
- gitlab_docs - gitlab_docs
- gitlab_handbook - gitlab_handbook
- gitter
- global_search - global_search
- helm_chart_registry - helm_chart_registry
- importers - importers
@ -101,6 +99,8 @@
- secret_detection - secret_detection
- secrets_management - secrets_management
- security_benchmarking - security_benchmarking
- security_orchestration
- self_monitoring
- serverless - serverless
- service_desk - service_desk
- snippets - snippets
@ -108,12 +108,13 @@
- static_application_security_testing - static_application_security_testing
- static_site_editor - static_site_editor
- subgroups - subgroups
- synthetic_monitoring
- templates - templates
- time_tracking - time_tracking
- tracing - tracing
- usability_testing - usability_testing
- users - users
- value_stream_management - value_stream_analytics
- vulnerability_database - vulnerability_database
- vulnerability_management - vulnerability_management
- web_firewall - web_firewall

View File

@ -171,7 +171,7 @@ Example response:
} }
``` ```
## Create an issue board **(STARTER)** ## Create an issue board
Creates a project issue board. Creates a project issue board.
@ -248,7 +248,7 @@ Example response:
} }
``` ```
## Update an issue board **(STARTER)** ## Update an issue board
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
@ -258,15 +258,15 @@ Updates a project issue board.
PUT /projects/:id/boards/:board_id PUT /projects/:id/boards/:board_id
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| ------------------- | -------------- | -------- | ----------- | | ---------------------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board | | `board_id` | integer | yes | The ID of a board |
| `name` | string | no | The new name of the board | | `name` | string | no | The new name of the board |
| `assignee_id` | integer | no | The assignee the board should be scoped to | | `assignee_id` **(STARTER)** | integer | no | The assignee the board should be scoped to |
| `milestone_id` | integer | no | The milestone the board should be scoped to | | `milestone_id` **(STARTER)** | integer | no | The milestone the board should be scoped to |
| `labels` | string | no | Comma-separated list of label names which the board should be scoped to | | `labels` **(STARTER)** | string | no | Comma-separated list of label names which the board should be scoped to |
| `weight` | integer | no | The weight range from 0 to 9, to which the board should be scoped to | | `weight` **(STARTER)** | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
```shell ```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4" curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4"
@ -329,7 +329,7 @@ Example response:
} }
``` ```
## Delete an issue board **(STARTER)** ## Delete an issue board
Deletes a project issue board. Deletes a project issue board.

View File

@ -422,7 +422,7 @@ For updates, you can follow:
- [The epic tracking feature parity with project wikis](https://gitlab.com/groups/gitlab-org/-/epics/2782). - [The epic tracking feature parity with project wikis](https://gitlab.com/groups/gitlab-org/-/epics/2782).
- [The issue for adding the ability to move group wikis using the API](https://gitlab.com/gitlab-org/gitlab/-/issues/219003). - [The issue for adding the ability to move group wikis using the API](https://gitlab.com/gitlab-org/gitlab/-/issues/219003).
### Enable or disable group wikis **(CORE ONLY)** ### Enable or disable group wikis **(PREMIUM ONLY)**
Group wikis are under development but ready for production use. Group wikis are under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**. It is deployed behind a feature flag that is **enabled by default**.

View File

@ -282,7 +282,7 @@ module API
end end
end end
route :any, '*path' do route :any, '*path', feature_category: :not_owned do
error!('404 Not Found', 404) error!('404 Not Found', 404)
end end
end end

View File

@ -7,6 +7,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :container_registry
namespace 'registry' do namespace 'registry' do
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'

View File

@ -20,12 +20,16 @@ module API
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def retrieve_members(source, params:, deep: false) def retrieve_members(source, params:, deep: false)
members = deep ? find_all_members(source) : source.members.where.not(user_id: nil) members = deep ? find_all_members(source) : source_members(source).where.not(user_id: nil)
members = members.includes(:user) members = members.includes(:user)
members = members.references(:user).merge(User.search(params[:query])) if params[:query].present? members = members.references(:user).merge(User.search(params[:query])) if params[:query].present?
members = members.where(user_id: params[:user_ids]) if params[:user_ids].present? members = members.where(user_id: params[:user_ids]) if params[:user_ids].present?
members members
end end
def source_members(source)
source.members
end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def find_all_members(source) def find_all_members(source)

View File

@ -128,13 +128,13 @@ module API
# changes - changes as "oldrev newrev ref", see Gitlab::ChangesList # changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
# check_ip - optional, only in EE version, may limit access to # check_ip - optional, only in EE version, may limit access to
# group resources based on its IP restrictions # group resources based on its IP restrictions
post "/allowed" do post "/allowed", feature_category: :source_code_management do
# It was moved to a separate method so that EE can alter its behaviour more # It was moved to a separate method so that EE can alter its behaviour more
# easily. # easily.
check_allowed(params) check_allowed(params)
end end
post "/lfs_authenticate" do post "/lfs_authenticate", feature_category: :source_code_management do
status 200 status 200
unless actor.key_or_user unless actor.key_or_user
@ -152,7 +152,7 @@ module API
# Get a ssh key using the fingerprint # Get a ssh key using the fingerprint
# #
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
get '/authorized_keys' do get '/authorized_keys', feature_category: :source_code_management do
fingerprint = params.fetch(:fingerprint) do fingerprint = params.fetch(:fingerprint) do
Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint
end end
@ -165,11 +165,11 @@ module API
# #
# Discover user by ssh key, user id or username # Discover user by ssh key, user id or username
# #
get '/discover' do get '/discover', feature_category: :authentication_and_authorization do
present actor.user, with: Entities::UserSafe present actor.user, with: Entities::UserSafe
end end
get '/check' do get '/check', feature_category: :not_owned do
{ {
api_version: API.version, api_version: API.version,
gitlab_version: Gitlab::VERSION, gitlab_version: Gitlab::VERSION,
@ -178,7 +178,7 @@ module API
} }
end end
post '/two_factor_recovery_codes' do post '/two_factor_recovery_codes', feature_category: :authentication_and_authorization do
status 200 status 200
actor.update_last_used_at! actor.update_last_used_at!
@ -207,7 +207,7 @@ module API
{ success: true, recovery_codes: codes } { success: true, recovery_codes: codes }
end end
post '/personal_access_token' do post '/personal_access_token', feature_category: :authentication_and_authorization do
status 200 status 200
actor.update_last_used_at! actor.update_last_used_at!
@ -257,7 +257,7 @@ module API
{ success: true, token: access_token.token, scopes: access_token.scopes, expires_at: access_token.expires_at } { success: true, token: access_token.token, scopes: access_token.scopes, expires_at: access_token.expires_at }
end end
post '/pre_receive' do post '/pre_receive', feature_category: :source_code_management do
status 200 status 200
reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase
@ -265,7 +265,7 @@ module API
{ reference_counter_increased: reference_counter_increased } { reference_counter_increased: reference_counter_increased }
end end
post '/post_receive' do post '/post_receive', feature_category: :source_code_management do
status 200 status 200
response = PostReceiveService.new(actor.user, repository, project, params).execute response = PostReceiveService.new(actor.user, repository, project, params).execute
@ -273,7 +273,7 @@ module API
present response, with: Entities::InternalPostReceive::Response present response, with: Entities::InternalPostReceive::Response
end end
post '/two_factor_config' do post '/two_factor_config', feature_category: :authentication_and_authorization do
status 200 status 200
break { success: false } unless Feature.enabled?(:two_factor_for_cli) break { success: false } unless Feature.enabled?(:two_factor_for_cli)
@ -295,7 +295,7 @@ module API
end end
end end
post '/two_factor_otp_check' do post '/two_factor_otp_check', feature_category: :authentication_and_authorization do
status 200 status 200
break { success: false } unless Feature.enabled?(:two_factor_for_cli) break { success: false } unless Feature.enabled?(:two_factor_for_cli)

View File

@ -4,6 +4,8 @@ module API
# Kubernetes Internal API # Kubernetes Internal API
module Internal module Internal
class Kubernetes < ::API::Base class Kubernetes < ::API::Base
feature_category :kubernetes_management
before do before do
check_feature_enabled check_feature_enabled
authenticate_gitlab_kas_request! authenticate_gitlab_kas_request!

View File

@ -7,6 +7,8 @@ module API
before { authenticate_by_gitlab_shell_token! } before { authenticate_by_gitlab_shell_token! }
feature_category :source_code_management
helpers do helpers do
def find_lfs_object(lfs_oid) def find_lfs_object(lfs_oid)
LfsObject.find_by_oid(lfs_oid) LfsObject.find_by_oid(lfs_oid)

View File

@ -4,6 +4,8 @@ module API
# Pages Internal API # Pages Internal API
module Internal module Internal
class Pages < ::API::Base class Pages < ::API::Base
feature_category :pages
before do before do
authenticate_gitlab_pages_request! authenticate_gitlab_pages_request!
end end

View File

@ -136,7 +136,7 @@ module API
source = find_source(source_type, params.delete(:id)) source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source) authorize_admin_source!(source_type, source)
member = source.members.find_by!(user_id: params[:user_id]) member = source_members(source).find_by!(user_id: params[:user_id])
updated_member = updated_member =
::Members::UpdateService ::Members::UpdateService
.new(current_user, declared_params(include_missing: false)) .new(current_user, declared_params(include_missing: false))
@ -159,7 +159,7 @@ module API
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
delete ":id/members/:user_id" do delete ":id/members/:user_id" do
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
member = source.members.find_by!(user_id: params[:user_id]) member = source_members(source).find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do destroy_conditionally!(member) do
::Members::DestroyService.new(current_user).execute(member, unassign_issuables: params[:unassign_issuables]) ::Members::DestroyService.new(current_user).execute(member, unassign_issuables: params[:unassign_issuables])

View File

@ -4,6 +4,8 @@ module API
class PersonalAccessTokens < ::API::Base class PersonalAccessTokens < ::API::Base
include ::API::PaginationParams include ::API::PaginationParams
feature_category :authentication_and_authorization
desc 'Get all Personal Access Tokens' do desc 'Get all Personal Access Tokens' do
detail 'This feature was added in GitLab 13.3' detail 'This feature was added in GitLab 13.3'
success Entities::PersonalAccessToken success Entities::PersonalAccessToken

View File

@ -22,6 +22,8 @@ module API
include PaginationParams include PaginationParams
feature_category :integrations
before do before do
authorize_jira_user_agent!(request) authorize_jira_user_agent!(request)
authenticate! authenticate!

View File

@ -9,20 +9,20 @@ module Gitlab
APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance' ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
WHITELISTED_GIT_ROUTES = { ALLOWLISTED_GIT_ROUTES = {
'repositories/git_http' => %w{git_upload_pack git_receive_pack} 'repositories/git_http' => %w{git_upload_pack git_receive_pack}
}.freeze }.freeze
WHITELISTED_GIT_LFS_ROUTES = { ALLOWLISTED_GIT_LFS_ROUTES = {
'repositories/lfs_api' => %w{batch}, 'repositories/lfs_api' => %w{batch},
'repositories/lfs_locks_api' => %w{verify create unlock} 'repositories/lfs_locks_api' => %w{verify create unlock}
}.freeze }.freeze
WHITELISTED_GIT_REVISION_ROUTES = { ALLOWLISTED_GIT_REVISION_ROUTES = {
'projects/compare' => %w{create} 'projects/compare' => %w{create}
}.freeze }.freeze
WHITELISTED_SESSION_ROUTES = { ALLOWLISTED_SESSION_ROUTES = {
'sessions' => %w{destroy}, 'sessions' => %w{destroy},
'admin/sessions' => %w{create destroy} 'admin/sessions' => %w{create destroy}
}.freeze }.freeze
@ -55,7 +55,7 @@ module Gitlab
def disallowed_request? def disallowed_request?
DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) && DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) &&
!whitelisted_routes !allowlisted_routes
end end
def json_request? def json_request?
@ -87,7 +87,7 @@ module Gitlab
end end
# Overridden in EE module # Overridden in EE module
def whitelisted_routes def allowlisted_routes
workhorse_passthrough_route? || internal_route? || lfs_route? || compare_git_revisions_route? || sidekiq_route? || session_route? || graphql_query? workhorse_passthrough_route? || internal_route? || lfs_route? || compare_git_revisions_route? || sidekiq_route? || session_route? || graphql_query?
end end
@ -98,7 +98,7 @@ module Gitlab
return false unless request.post? && return false unless request.post? &&
request.path.end_with?('.git/git-upload-pack', '.git/git-receive-pack') request.path.end_with?('.git/git-upload-pack', '.git/git-receive-pack')
WHITELISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) ALLOWLISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end end
def internal_route? def internal_route?
@ -109,7 +109,7 @@ module Gitlab
# Calling route_hash may be expensive. Only do it if we think there's a possible match # Calling route_hash may be expensive. Only do it if we think there's a possible match
return false unless request.post? && request.path.end_with?('compare') return false unless request.post? && request.path.end_with?('compare')
WHITELISTED_GIT_REVISION_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) ALLOWLISTED_GIT_REVISION_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end end
def lfs_route? def lfs_route?
@ -120,7 +120,7 @@ module Gitlab
return false return false
end end
WHITELISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) ALLOWLISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end end
def session_route? def session_route?
@ -128,7 +128,7 @@ module Gitlab
return false unless request.post? && request.path.end_with?('/users/sign_out', return false unless request.post? && request.path.end_with?('/users/sign_out',
'/admin/session', '/admin/session/destroy') '/admin/session', '/admin/session/destroy')
WHITELISTED_SESSION_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) ALLOWLISTED_SESSION_ROUTES[route_hash[:controller]]&.include?(route_hash[:action])
end end
def sidekiq_route? def sidekiq_route?

View File

@ -14334,6 +14334,9 @@ msgstr ""
msgid "InstanceAnalytics|Total" msgid "InstanceAnalytics|Total"
msgstr "" msgstr ""
msgid "InstanceStatistics|Could not load the projects and groups chart. Please refresh the page to try again."
msgstr ""
msgid "InstanceStatistics|Groups" msgid "InstanceStatistics|Groups"
msgstr "" msgstr ""
@ -14343,12 +14346,30 @@ msgstr ""
msgid "InstanceStatistics|Merge Requests" msgid "InstanceStatistics|Merge Requests"
msgstr "" msgstr ""
msgid "InstanceStatistics|No data available."
msgstr ""
msgid "InstanceStatistics|Pipelines" msgid "InstanceStatistics|Pipelines"
msgstr "" msgstr ""
msgid "InstanceStatistics|Projects" msgid "InstanceStatistics|Projects"
msgstr "" msgstr ""
msgid "InstanceStatistics|There was an error while loading the groups"
msgstr ""
msgid "InstanceStatistics|There was an error while loading the projects"
msgstr ""
msgid "InstanceStatistics|Total groups"
msgstr ""
msgid "InstanceStatistics|Total projects"
msgstr ""
msgid "InstanceStatistics|Total projects & groups"
msgstr ""
msgid "InstanceStatistics|Users" msgid "InstanceStatistics|Users"
msgstr "" msgstr ""

View File

@ -90,7 +90,7 @@ function rspec_simple_job() {
export NO_KNAPSACK="1" export NO_KNAPSACK="1"
bin/rspec --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts} bin/rspec -Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}
} }
function rspec_paralellized_job() { function rspec_paralellized_job() {
@ -143,7 +143,7 @@ function rspec_paralellized_job() {
export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv" export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv"
knapsack rspec "-Ispec --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}" knapsack rspec "-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
date date
} }

View File

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
# When running in CI environment, we need to load a full `spec_helper` # $" is $LOADED_FEATURES, but RuboCop didn't like it
if ENV['CI'] if $".include?(File.expand_path('spec_helper.rb', __dir__))
require_relative 'spec_helper' # There's no need to load anything here if spec_helper is already loaded
# because spec_helper is more extensive than fast_spec_helper
return return
end end

View File

@ -1,4 +1,9 @@
const defaultPageInfo = { hasPreviousPage: false, startCursor: null, endCursor: null }; const defaultPageInfo = {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
};
export function getApolloResponse(options = {}) { export function getApolloResponse(options = {}) {
const { const {
@ -28,3 +33,38 @@ export function getApolloResponse(options = {}) {
}, },
}; };
} }
const mockApolloResponse = ({ hasNextPage = false, key, data }) => ({
data: {
[key]: {
pageInfo: { ...defaultPageInfo, hasNextPage },
nodes: data,
},
},
});
export const mockQueryResponse = ({
key,
data = [],
loading = false,
hasNextPage = false,
additionalData = [],
}) => {
const response = mockApolloResponse({ hasNextPage, key, data });
if (loading) {
return jest.fn().mockReturnValue(new Promise(() => {}));
}
if (hasNextPage) {
return jest
.fn()
.mockResolvedValueOnce(response)
.mockResolvedValueOnce(
mockApolloResponse({
hasNextPage: false,
key,
data: additionalData,
}),
);
}
return jest.fn().mockResolvedValue(response);
};

View File

@ -3,6 +3,7 @@ import InstanceStatisticsApp from '~/analytics/instance_statistics/components/ap
import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue'; import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue';
import InstanceStatisticsCountChart from '~/analytics/instance_statistics/components/instance_statistics_count_chart.vue'; import InstanceStatisticsCountChart from '~/analytics/instance_statistics/components/instance_statistics_count_chart.vue';
import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue'; import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue';
import ProjectsAndGroupsChart from '~/analytics/instance_statistics/components/projects_and_groups_chart.vue';
describe('InstanceStatisticsApp', () => { describe('InstanceStatisticsApp', () => {
let wrapper; let wrapper;
@ -34,4 +35,8 @@ describe('InstanceStatisticsApp', () => {
it('displays the users chart component', () => { it('displays the users chart component', () => {
expect(wrapper.find(UsersChart).exists()).toBe(true); expect(wrapper.find(UsersChart).exists()).toBe(true);
}); });
it('displays the projects and groups chart component', () => {
expect(wrapper.find(ProjectsAndGroupsChart).exists()).toBe(true);
});
}); });

View File

@ -0,0 +1,218 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { useFakeDate } from 'helpers/fake_date';
import ProjectsAndGroupChart from '~/analytics/instance_statistics/components/projects_and_groups_chart.vue';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import projectsQuery from '~/analytics/instance_statistics/graphql/queries/projects.query.graphql';
import groupsQuery from '~/analytics/instance_statistics/graphql/queries/groups.query.graphql';
import { mockCountsData2, roundedSortedCountsMonthlyChartData2 } from '../mock_data';
import { mockQueryResponse } from '../apollo_mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('ProjectsAndGroupChart', () => {
let wrapper;
let queryResponses = { projects: null, groups: null };
const mockAdditionalData = [{ recordedAt: '2020-07-21', count: 5 }];
const createComponent = ({
loadingError = false,
projects = [],
groups = [],
projectsLoading = false,
groupsLoading = false,
projectsHasNextPage = false,
groupsHasNextPage = false,
} = {}) => {
queryResponses = {
projects: mockQueryResponse({
key: 'projects',
data: projects,
loading: projectsLoading,
hasNextPage: projectsHasNextPage,
additionalData: mockAdditionalData,
}),
groups: mockQueryResponse({
key: 'groups',
data: groups,
loading: groupsLoading,
hasNextPage: groupsHasNextPage,
additionalData: mockAdditionalData,
}),
};
return shallowMount(ProjectsAndGroupChart, {
props: {
startDate: useFakeDate(2020, 9, 26),
endDate: useFakeDate(2020, 10, 1),
totalDataPoints: mockCountsData2.length,
},
localVue,
apolloProvider: createMockApollo([
[projectsQuery, queryResponses.projects],
[groupsQuery, queryResponses.groups],
]),
data() {
return { loadingError };
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
queryResponses = {
projects: null,
groups: null,
};
});
const findLoader = () => wrapper.find(ChartSkeletonLoader);
const findAlert = () => wrapper.find(GlAlert);
const findChart = () => wrapper.find(GlLineChart);
describe('while loading', () => {
beforeEach(() => {
wrapper = createComponent({ projectsLoading: true, groupsLoading: true });
});
it('displays the skeleton loader', () => {
expect(findLoader().exists()).toBe(true);
});
it('hides the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe('while loading 1 data set', () => {
beforeEach(async () => {
wrapper = createComponent({
projects: mockCountsData2,
groupsLoading: true,
});
await wrapper.vm.$nextTick();
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('renders the chart', () => {
expect(findChart().exists()).toBe(true);
});
});
describe('without data', () => {
beforeEach(async () => {
wrapper = createComponent({ projects: [] });
await wrapper.vm.$nextTick();
});
it('renders a no data message', () => {
expect(findAlert().text()).toBe('No data available.');
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('does not render the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe('with data', () => {
beforeEach(async () => {
wrapper = createComponent({ projects: mockCountsData2 });
await wrapper.vm.$nextTick();
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('renders the chart', () => {
expect(findChart().exists()).toBe(true);
});
it('passes the data to the line chart', () => {
expect(findChart().props('data')).toEqual([
{ data: roundedSortedCountsMonthlyChartData2, name: 'Total projects' },
{ data: [], name: 'Total groups' },
]);
});
});
describe('with errors', () => {
beforeEach(async () => {
wrapper = createComponent({ loadingError: true });
await wrapper.vm.$nextTick();
});
it('renders an error message', () => {
expect(findAlert().text()).toBe('No data available.');
});
it('hides the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('hides the chart', () => {
expect(findChart().exists()).toBe(false);
});
});
describe.each`
metric | loadingState | newData
${'projects'} | ${{ projectsHasNextPage: true }} | ${{ projects: mockCountsData2 }}
${'groups'} | ${{ groupsHasNextPage: true }} | ${{ groups: mockCountsData2 }}
`('$metric - fetchMore', ({ metric, loadingState, newData }) => {
describe('when the fetchMore query returns data', () => {
beforeEach(async () => {
wrapper = createComponent({
...loadingState,
...newData,
});
jest.spyOn(wrapper.vm.$apollo.queries[metric], 'fetchMore');
await wrapper.vm.$nextTick();
});
it('requests data twice', () => {
expect(queryResponses[metric]).toBeCalledTimes(2);
});
it('calls fetchMore', () => {
expect(wrapper.vm.$apollo.queries[metric].fetchMore).toHaveBeenCalledTimes(1);
});
});
describe('when the fetchMore query throws an error', () => {
beforeEach(() => {
wrapper = createComponent({
...loadingState,
...newData,
});
jest
.spyOn(wrapper.vm.$apollo.queries[metric], 'fetchMore')
.mockImplementation(jest.fn().mockRejectedValue());
return wrapper.vm.$nextTick();
});
it('calls fetchMore', () => {
expect(wrapper.vm.$apollo.queries[metric].fetchMore).toHaveBeenCalledTimes(1);
});
it('renders an error message', () => {
expect(findAlert().text()).toBe('No data available.');
});
});
});
});

View File

@ -7,7 +7,8 @@ import { useFakeDate } from 'helpers/fake_date';
import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue'; import UsersChart from '~/analytics/instance_statistics/components/users_chart.vue';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
import usersQuery from '~/analytics/instance_statistics/graphql/queries/users.query.graphql'; import usersQuery from '~/analytics/instance_statistics/graphql/queries/users.query.graphql';
import { mockCountsData2, roundedSortedCountsMonthlyChartData2, mockPageInfo } from '../mock_data'; import { mockCountsData2, roundedSortedCountsMonthlyChartData2 } from '../mock_data';
import { mockQueryResponse } from '../apollo_mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
@ -16,43 +17,13 @@ describe('UsersChart', () => {
let wrapper; let wrapper;
let queryHandler; let queryHandler;
const mockApolloResponse = ({ loading = false, hasNextPage = false, users }) => ({
data: {
users: {
pageInfo: { ...mockPageInfo, hasNextPage },
nodes: users,
loading,
},
},
});
const mockQueryResponse = ({ users, loading = false, hasNextPage = false }) => {
const apolloQueryResponse = mockApolloResponse({ loading, hasNextPage, users });
if (loading) {
return jest.fn().mockReturnValue(new Promise(() => {}));
}
if (hasNextPage) {
return jest
.fn()
.mockResolvedValueOnce(apolloQueryResponse)
.mockResolvedValueOnce(
mockApolloResponse({
loading,
hasNextPage: false,
users: [{ recordedAt: '2020-07-21', count: 5 }],
}),
);
}
return jest.fn().mockResolvedValue(apolloQueryResponse);
};
const createComponent = ({ const createComponent = ({
loadingError = false, loadingError = false,
loading = false, loading = false,
users = [], users = [],
hasNextPage = false, hasNextPage = false,
} = {}) => { } = {}) => {
queryHandler = mockQueryResponse({ users, loading, hasNextPage }); queryHandler = mockQueryResponse({ key: 'users', data: users, loading, hasNextPage });
return shallowMount(UsersChart, { return shallowMount(UsersChart, {
props: { props: {

View File

@ -33,10 +33,3 @@ export const roundedSortedCountsMonthlyChartData2 = [
['2020-06-01', 21], // average of 2020-06-x items ['2020-06-01', 21], // average of 2020-06-x items
['2020-07-01', 10], // average of 2020-07-x items ['2020-07-01', 10], // average of 2020-07-x items
]; ];
export const mockPageInfo = {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
};

View File

@ -18,46 +18,6 @@ RSpec.describe 'Every API endpoint' do
api_endpoints.map do |(klass, path)| api_endpoints.map do |(klass, path)|
next if klass.try(:feature_category_for_action, path) next if klass.try(:feature_category_for_action, path)
# We'll add the rest in https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463
completed_classes = [
::API::Users, ::API::Issues, ::API::AccessRequests, ::API::Admin::Ci::Variables,
::API::Admin::InstanceClusters, ::API::Admin::Sidekiq, ::API::Appearance,
::API::Applications, ::API::Avatar, ::API::AwardEmoji, API::Badges,
::API::Boards, ::API::Branches, ::API::BroadcastMessages, ::API::Ci::Pipelines,
::API::Ci::PipelineSchedules, ::API::Ci::Runners, ::API::Ci::Runner,
::API::Commits, ::API::CommitStatuses, ::API::ContainerRegistryEvent,
::API::DeployKeys, ::API::DeployTokens, ::API::Deployments, ::API::Environments,
::API::ErrorTracking, ::API::Events, ::API::FeatureFlags, ::API::FeatureFlagScopes,
::API::FeatureFlagsUserLists, ::API::Features, ::API::Files, ::API::FreezePeriods,
::API::GroupBoards, ::API::GroupClusters, ::API::GroupExport, ::API::GroupImport,
::API::GroupLabels, ::API::GroupMilestones, ::API::Groups,
::API::GroupContainerRepositories, ::API::GroupVariables,
::API::ImportBitbucketServer, ::API::ImportGithub, ::API::IssueLinks,
::API::Issues, ::API::JobArtifacts, ::API::Jobs, ::API::Keys, ::API::Labels,
::API::Lint, ::API::Markdown, ::API::Members, ::API::MergeRequestDiffs,
::API::MergeRequests, ::API::MergeRequestApprovals, ::API::Metrics::Dashboard::Annotations,
::API::Metrics::UserStarredDashboards, ::API::Namespaces, ::API::Notes,
::API::Discussions, ::API::ResourceLabelEvents, ::API::ResourceMilestoneEvents,
::API::ResourceStateEvents, ::API::NotificationSettings, ::API::ProjectPackages,
::API::GroupPackages, ::API::PackageFiles, ::API::NugetPackages, ::API::PypiPackages,
::API::ComposerPackages, ::API::ConanProjectPackages, ::API::ConanInstancePackages,
::API::DebianGroupPackages, ::API::DebianProjectPackages, ::API::MavenPackages,
::API::NpmPackages, ::API::GenericPackages, ::API::GoProxy, ::API::Pages,
::API::PagesDomains, ::API::ProjectClusters, ::API::ProjectContainerRepositories,
::API::ProjectEvents, ::API::ProjectExport, ::API::ProjectImport, ::API::ProjectHooks,
::API::ProjectMilestones, ::API::ProjectRepositoryStorageMoves, ::API::Projects,
::API::ProjectSnapshots, ::API::ProjectSnippets, ::API::ProjectStatistics,
::API::ProjectTemplates, ::API::Terraform::State, ::API::Terraform::StateVersion,
::API::ProtectedBranches, ::API::ProtectedTags, ::API::Releases, ::API::Release::Links,
::API::RemoteMirrors, ::API::Repositories, ::API::Search, ::API::Services,
::API::Settings, ::API::SidekiqMetrics, ::API::Snippets, ::API::Statistics,
::API::Submodules, ::API::Subscriptions, ::API::Suggestions, ::API::SystemHooks,
::API::Tags, ::API::Templates, ::API::Todos, ::API::Triggers, ::API::Unleash,
::API::UsageData, ::API::UserCounts, ::API::Variables, ::API::Version,
::API::Wikis
]
next unless completed_classes.include?(klass)
"#{klass}##{path}" "#{klass}##{path}"
end.compact.uniq end.compact.uniq
end end

View File

@ -147,42 +147,45 @@ RSpec.describe Namespace do
end end
describe '.search' do describe '.search' do
let_it_be(:namespace) { create(:namespace) } let_it_be(:first_namespace) { build(:namespace, name: 'my first namespace', path: 'old-path').tap(&:save!) }
let_it_be(:parent_namespace) { build(:namespace, name: 'my parent namespace', path: 'parent-path').tap(&:save!) }
let_it_be(:second_namespace) { build(:namespace, name: 'my second namespace', path: 'new-path', parent: parent_namespace).tap(&:save!) }
let_it_be(:project_with_same_path) { create(:project, id: second_namespace.id, path: first_namespace.path) }
it 'returns namespaces with a matching name' do it 'returns namespaces with a matching name' do
expect(described_class.search(namespace.name)).to eq([namespace]) expect(described_class.search('my first namespace')).to eq([first_namespace])
end end
it 'returns namespaces with a partially matching name' do it 'returns namespaces with a partially matching name' do
expect(described_class.search(namespace.name[0..2])).to eq([namespace]) expect(described_class.search('first')).to eq([first_namespace])
end end
it 'returns namespaces with a matching name regardless of the casing' do it 'returns namespaces with a matching name regardless of the casing' do
expect(described_class.search(namespace.name.upcase)).to eq([namespace]) expect(described_class.search('MY FIRST NAMESPACE')).to eq([first_namespace])
end end
it 'returns namespaces with a matching path' do it 'returns namespaces with a matching path' do
expect(described_class.search(namespace.path)).to eq([namespace]) expect(described_class.search('old-path')).to eq([first_namespace])
end end
it 'returns namespaces with a partially matching path' do it 'returns namespaces with a partially matching path' do
expect(described_class.search(namespace.path[0..2])).to eq([namespace]) expect(described_class.search('old')).to eq([first_namespace])
end end
it 'returns namespaces with a matching path regardless of the casing' do it 'returns namespaces with a matching path regardless of the casing' do
expect(described_class.search(namespace.path.upcase)).to eq([namespace]) expect(described_class.search('OLD-PATH')).to eq([first_namespace])
end end
it 'returns namespaces with a matching route path' do it 'returns namespaces with a matching route path' do
expect(described_class.search(namespace.route.path, include_parents: true)).to eq([namespace]) expect(described_class.search('parent-path/new-path', include_parents: true)).to eq([second_namespace])
end end
it 'returns namespaces with a partially matching route path' do it 'returns namespaces with a partially matching route path' do
expect(described_class.search(namespace.route.path[0..2], include_parents: true)).to eq([namespace]) expect(described_class.search('parent-path/new', include_parents: true)).to eq([second_namespace])
end end
it 'returns namespaces with a matching route path regardless of the casing' do it 'returns namespaces with a matching route path regardless of the casing' do
expect(described_class.search(namespace.route.path.upcase, include_parents: true)).to eq([namespace]) expect(described_class.search('PARENT-PATH/NEW-PATH', include_parents: true)).to eq([second_namespace])
end end
end end

View File

@ -62,6 +62,15 @@ RSpec.describe Route do
end end
end end
describe '.for_routable_type' do
let!(:nested_group) { create(:group, path: 'foo', name: 'foo', parent: group) }
let!(:project) { create(:project, path: 'other-project') }
it 'returns correct routes' do
expect(described_class.for_routable_type(Project.name)).to match_array([project.route])
end
end
describe '#rename_descendants' do describe '#rename_descendants' do
let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) } let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) }
let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) } let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) }

View File

@ -7,6 +7,7 @@ RSpec.describe API::Members do
let(:developer) { create(:user) } let(:developer) { create(:user) }
let(:access_requester) { create(:user) } let(:access_requester) { create(:user) }
let(:stranger) { create(:user) } let(:stranger) { create(:user) }
let(:user_with_minimal_access) { create(:user) }
let(:project) do let(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project| create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
@ -20,6 +21,7 @@ RSpec.describe API::Members do
create(:group, :public) do |group| create(:group, :public) do |group|
group.add_developer(developer) group.add_developer(developer)
group.add_owner(maintainer) group.add_owner(maintainer)
create(:group_member, :minimal_access, source: group, user: user_with_minimal_access)
group.request_access(access_requester) group.request_access(access_requester)
end end
end end

View File

@ -1,5 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
# $" is $LOADED_FEATURES, but RuboCop didn't like it
if $".include?(File.expand_path('fast_spec_helper.rb', __dir__))
warn 'Detected fast_spec_helper is loaded first than spec_helper.'
warn 'If running test files using both spec_helper and fast_spec_helper,'
warn 'make sure test file with spec_helper is loaded first.'
abort 'Aborting...'
end
require './spec/simplecov_env' require './spec/simplecov_env'
SimpleCovEnv.start! SimpleCovEnv.start!