Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-15 12:07:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-15 12:07:33 +0300
commit389d5aa505a916b0506b7b73dcc3be342d724976 (patch)
treed53490630b9aac2eb397d9816db91030afad832a
parentf632ee795fd542d7ae2753d157725bd39d33e32d (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue7
-rw-r--r--app/assets/javascripts/pipelines/pipelines_index.js2
-rw-r--r--app/assets/javascripts/reports/constants.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js82
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js55
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue16
-rw-r--r--app/controllers/projects/ci/secure_files_controller.rb8
-rw-r--r--app/helpers/ci/pipelines_helper.rb1
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/ci/destroy_secure_file_service.rb11
-rw-r--r--app/services/projects/destroy_service.rb4
-rw-r--r--config/feature_flags/development/container_registry_follow_redirects_middleware.yml2
-rw-r--r--db/migrate/20220217113058_add_status_to_status_check_responses.rb7
-rw-r--r--db/schema_migrations/202202171130581
-rw-r--r--db/structure.sql3
-rw-r--r--doc/api/index.md3
-rw-r--r--doc/api/notes.md9
-rw-r--r--doc/api/status_checks.md18
-rw-r--r--doc/ci/environments/deployment_approvals.md30
-rw-r--r--doc/ci/pipelines/merge_request_pipelines.md4
-rw-r--r--doc/ci/pipelines/merged_results_pipelines.md2
-rw-r--r--doc/development/code_review.md22
-rw-r--r--doc/development/product_qualified_lead_guide/index.md30
-rw-r--r--doc/install/docker.md16
-rw-r--r--doc/update/index.md6
-rw-r--r--lib/api/ci/secure_files.rb63
-rw-r--r--lib/api/notes.rb4
-rw-r--r--lib/container_registry/client.rb2
-rw-r--r--locale/gitlab.pot32
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js2
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js47
-rw-r--r--spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js149
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js4
-rw-r--r--spec/helpers/ci/pipelines_helper_spec.rb1
-rw-r--r--spec/requests/api/ci/secure_files_spec.rb186
-rw-r--r--spec/requests/api/notes_spec.rb89
-rw-r--r--spec/services/ci/destroy_secure_file_service_spec.rb32
-rw-r--r--spec/services/projects/destroy_service_spec.rb2
43 files changed, 505 insertions, 505 deletions
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index 5a94d144031..db9dc74863d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -98,6 +98,11 @@ export default {
type: String,
required: true,
},
+ defaultBranchName: {
+ type: String,
+ required: false,
+ default: null,
+ },
params: {
type: Object,
required: true,
@@ -347,6 +352,7 @@ export default {
<pipelines-filtered-search
class="gl-display-flex gl-flex-grow-1 gl-mr-4"
:project-id="projectId"
+ :default-branch-name="defaultBranchName"
:params="validatedParams"
@filterPipelines="filterPipelines"
/>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
index 2dfdaa0ea28..4d28545a035 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
@@ -24,6 +24,11 @@ export default {
type: String,
required: true,
},
+ defaultBranchName: {
+ type: String,
+ required: false,
+ default: null,
+ },
params: {
type: Object,
required: true,
@@ -57,6 +62,7 @@ export default {
token: PipelineBranchNameToken,
operators: OPERATOR_IS_ONLY,
projectId: this.projectId,
+ defaultBranchName: this.defaultBranchName,
disabled: this.selectedTypes.includes(this.$options.tagType),
},
{
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
index 5409e68cdc4..1db2898b72a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
@@ -35,6 +35,13 @@ export default {
Api.branches(this.config.projectId, searchterm)
.then(({ data }) => {
this.branches = data.map((branch) => branch.name);
+ if (!searchterm && this.config.defaultBranchName) {
+ // Shift the default branch to the top of the list
+ this.branches = this.branches.filter(
+ (branch) => branch !== this.config.defaultBranchName,
+ );
+ this.branches.unshift(this.config.defaultBranchName);
+ }
this.loading = false;
})
.catch((err) => {
diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js
index 2b1f5419953..f4d9a44a754 100644
--- a/app/assets/javascripts/pipelines/pipelines_index.js
+++ b/app/assets/javascripts/pipelines/pipelines_index.js
@@ -36,6 +36,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
ciLintPath,
resetCachePath,
projectId,
+ defaultBranchName,
params,
ciRunnerSettingsPath,
anyRunnersAvailable,
@@ -75,6 +76,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
ciLintPath,
resetCachePath,
projectId,
+ defaultBranchName,
params: JSON.parse(params),
ciRunnerSettingsPath,
anyRunnersAvailable: parseBoolean(anyRunnersAvailable),
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index 53273aeff33..bad6fa1e7b9 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -18,6 +18,7 @@ export const ICON_WARNING = 'warning';
export const ICON_SUCCESS = 'success';
export const ICON_NOTFOUND = 'notfound';
export const ICON_PENDING = 'pending';
+export const ICON_FAILED = 'failed';
export const status = {
LOADING,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index e5a6514624d..684386883c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -86,7 +86,7 @@ export default {
);
},
statusIconName() {
- if (this.hasFetchError) return EXTENSION_ICONS.failed;
+ if (this.hasFetchError) return EXTENSION_ICONS.error;
if (this.isLoadingSummary) return null;
return this.statusIcon(this.collapsedData);
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
deleted file mode 100644
index cd5cfb6837c..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/constants.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { __, n__, s__, sprintf } from '~/locale';
-
-const digitText = (bold = false) => (bold ? '%{strong_start}%d%{strong_end}' : '%d');
-const noText = (bold = false) => (bold ? '%{strong_start}no%{strong_end}' : 'no');
-
-export const TESTS_FAILED_STATUS = 'failed';
-export const ERROR_STATUS = 'error';
-
-export const i18n = {
- label: s__('Reports|Test summary'),
- loading: s__('Reports|Test summary results are loading'),
- error: s__('Reports|Test summary failed to load results'),
- fullReport: s__('Reports|Full report'),
-
- noChanges: (bold) => s__(`Reports|${noText(bold)} changed test results`),
- resultsString: (combinedString, resolvedString) =>
- sprintf(s__('Reports|%{combinedString} and %{resolvedString}'), {
- combinedString,
- resolvedString,
- }),
-
- summaryText: (name, resultsString) =>
- sprintf(__('%{name}: %{resultsString}'), { name, resultsString }),
-
- failedClause: (failed, bold) =>
- n__(`${digitText(bold)} failed`, `${digitText(bold)} failed`, failed),
- erroredClause: (errored, bold) =>
- n__(`${digitText(bold)} error`, `${digitText(bold)} errors`, errored),
- resolvedClause: (resolved, bold) =>
- n__(`${digitText(bold)} fixed test result`, `${digitText(bold)} fixed test results`, resolved),
- totalClause: (total, bold) =>
- n__(`${digitText(bold)} total test`, `${digitText(bold)} total tests`, total),
-
- reportError: s__('Reports|An error occurred while loading report'),
- reportErrorWithName: (name) =>
- sprintf(s__('Reports|An error occurred while loading %{name} results'), { name }),
- headReportParsingError: s__('Reports|Head report parsing error:'),
- baseReportParsingError: s__('Reports|Base report parsing error:'),
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
deleted file mode 100644
index 65d9257903f..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import { uniqueId } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
-import { EXTENSION_ICONS } from '../../constants';
-import { summaryTextBuilder, reportTextBuilder, reportSubTextBuilder } from './utils';
-import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
-
-export default {
- name: 'WidgetTestSummary',
- enablePolling: true,
- i18n,
- expandEvent: 'i_testing_summary_widget_total',
- props: ['testResultsPath', 'headBlobPath', 'pipeline'],
- computed: {
- summary(data) {
- if (data.parsingInProgress) {
- return this.$options.i18n.loading;
- }
- if (data.hasSuiteError) {
- return this.$options.i18n.error;
- }
- return summaryTextBuilder(this.$options.i18n.label, data.summary);
- },
- statusIcon(data) {
- if (data.parsingInProgress) {
- return null;
- }
- if (data.status === TESTS_FAILED_STATUS) {
- return EXTENSION_ICONS.warning;
- }
- if (data.hasSuiteError) {
- return EXTENSION_ICONS.failed;
- }
- return EXTENSION_ICONS.success;
- },
- tertiaryButtons() {
- return [
- {
- text: this.$options.i18n.fullReport,
- href: `${this.pipeline.path}/test_report`,
- target: '_blank',
- },
- ];
- },
- },
- methods: {
- fetchCollapsedData() {
- return axios.get(this.testResultsPath).then(({ data = {}, status }) => {
- return {
- data: {
- hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS),
- parsingInProgress: status === 204,
- ...data,
- },
- };
- });
- },
- fetchFullData() {
- return Promise.resolve(this.prepareReports());
- },
- suiteIcon(suite) {
- if (suite.status === ERROR_STATUS) {
- return EXTENSION_ICONS.error;
- }
- if (suite.status === TESTS_FAILED_STATUS) {
- return EXTENSION_ICONS.failed;
- }
- return EXTENSION_ICONS.success;
- },
- prepareReports() {
- return this.collapsedData.suites.map((suite) => {
- return {
- id: uniqueId('suite-'),
- text: reportTextBuilder(suite),
- subtext: reportSubTextBuilder(suite),
- icon: {
- name: this.suiteIcon(suite),
- },
- };
- });
- },
- },
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
deleted file mode 100644
index a74ed20362f..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { i18n } from './constants';
-
-const textBuilder = (results, boldNumbers = false) => {
- const { failed, errored, resolved, total } = results;
-
- const failedOrErrored = (failed || 0) + (errored || 0);
- const failedString = failed ? i18n.failedClause(failed, boldNumbers) : null;
- const erroredString = errored ? i18n.erroredClause(errored, boldNumbers) : null;
- const combinedString =
- failed && errored ? `${failedString}, ${erroredString}` : failedString || erroredString;
- const resolvedString = resolved ? i18n.resolvedClause(resolved, boldNumbers) : null;
- const totalString = total ? i18n.totalClause(total, boldNumbers) : null;
-
- let resultsString = i18n.noChanges(boldNumbers);
-
- if (failedOrErrored) {
- if (resolved) {
- resultsString = i18n.resultsString(combinedString, resolvedString);
- } else {
- resultsString = combinedString;
- }
- } else if (resolved) {
- resultsString = resolvedString;
- }
-
- return `${resultsString}, ${totalString}`;
-};
-
-export const summaryTextBuilder = (name = '', results = {}) => {
- const resultsString = textBuilder(results, true);
- return i18n.summaryText(name, resultsString);
-};
-
-export const reportTextBuilder = ({ name = '', summary = {}, status }) => {
- if (!name) {
- return i18n.reportError;
- }
- if (status === 'error') {
- return i18n.reportErrorWithName(name);
- }
-
- const resultsString = textBuilder(summary);
- return i18n.summaryText(name, resultsString);
-};
-
-export const reportSubTextBuilder = ({ suite_errors }) => {
- const errors = [];
- if (suite_errors?.head) {
- errors.push(`${i18n.headReportParsingError} ${suite_errors.head}`);
- }
- if (suite_errors?.base) {
- errors.push(`${i18n.baseReportParsingError} ${suite_errors.base}`);
- }
- return errors.join('<br />');
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index bb25fa15626..cd4d9398899 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -46,7 +46,6 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
import getStateQuery from './queries/get_state.query.graphql';
import terraformExtension from './extensions/terraform';
import accessibilityExtension from './extensions/accessibility';
-import testReportExtension from './extensions/test_report';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
@@ -191,9 +190,6 @@ export default {
shouldRenderTerraformPlans() {
return Boolean(this.mr?.terraformReportsPath);
},
- shouldRenderTestReport() {
- return Boolean(this.mr?.testResultsPath);
- },
mergeError() {
let { mergeError } = this.mr;
@@ -250,11 +246,6 @@ export default {
this.registerAccessibilityExtension();
}
},
- shouldRenderTestReport(newVal) {
- if (newVal) {
- this.registerTestReportExtension();
- }
- },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -500,11 +491,6 @@ export default {
registerExtension(accessibilityExtension);
}
},
- registerTestReportExtension() {
- if (this.shouldRenderTestReport && this.shouldShowExtension) {
- registerExtension(testReportExtension);
- }
- },
},
};
</script>
@@ -577,7 +563,7 @@ export default {
/>
<grouped-test-reports-app
- v-if="mr.testResultsPath && !shouldShowExtension"
+ v-if="mr.testResultsPath"
class="js-reports-container"
:endpoint="mr.testResultsPath"
:head-blob-path="mr.headBlobPath"
diff --git a/app/controllers/projects/ci/secure_files_controller.rb b/app/controllers/projects/ci/secure_files_controller.rb
index 4b6a6b19454..5141d0188b0 100644
--- a/app/controllers/projects/ci/secure_files_controller.rb
+++ b/app/controllers/projects/ci/secure_files_controller.rb
@@ -1,16 +1,10 @@
# frozen_string_literal: true
class Projects::Ci::SecureFilesController < Projects::ApplicationController
- before_action :check_can_collaborate!
+ before_action :authorize_read_secure_files!
feature_category :pipeline_authoring
def show
end
-
- private
-
- def check_can_collaborate!
- render_404 unless can_collaborate_with_project?(project)
- end
end
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index d742842b8b6..8d2f83409be 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -84,6 +84,7 @@ module Ci
data = {
endpoint: list_url,
project_id: project.id,
+ default_branch_name: project.default_branch,
params: params.to_json,
artifacts_endpoint: downloadable_artifacts_project_pipeline_path(project, artifacts_endpoint_placeholder, format: :json),
artifacts_endpoint_placeholder: artifacts_endpoint_placeholder,
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 069096e5bc0..e7b63d5e17f 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -413,6 +413,7 @@ class ProjectPolicy < BasePolicy
enable :admin_feature_flag
enable :admin_feature_flags_user_lists
enable :update_escalation_status
+ enable :read_secure_files
end
rule { can?(:developer_access) & user_confirmed? }.policy do
@@ -462,6 +463,7 @@ class ProjectPolicy < BasePolicy
enable :register_project_runners
enable :update_runners_registration_token
enable :admin_project_google_cloud
+ enable :admin_secure_files
end
rule { public_project & metrics_dashboard_allowed }.policy do
diff --git a/app/services/ci/destroy_secure_file_service.rb b/app/services/ci/destroy_secure_file_service.rb
new file mode 100644
index 00000000000..7145ace7f31
--- /dev/null
+++ b/app/services/ci/destroy_secure_file_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Ci
+ class DestroySecureFileService < BaseService
+ def execute(secure_file)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_secure_files, secure_file.project)
+
+ secure_file.destroy!
+ end
+ end
+end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index adbc26420c7..a73244c6971 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -200,6 +200,10 @@ module Projects
::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
end
+ project.secure_files.find_each(batch_size: BATCH_SIZE) do |secure_file| # rubocop: disable CodeReuse/ActiveRecord
+ ::Ci::DestroySecureFileService.new(project, current_user).execute(secure_file)
+ end
+
deleted_count = ::CommitStatus.for_project(project).delete_all
Gitlab::AppLogger.info(
diff --git a/config/feature_flags/development/container_registry_follow_redirects_middleware.yml b/config/feature_flags/development/container_registry_follow_redirects_middleware.yml
index 6e78c0ea295..6b0ded9dbc4 100644
--- a/config/feature_flags/development/container_registry_follow_redirects_middleware.yml
+++ b/config/feature_flags/development/container_registry_follow_redirects_middleware.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353291
milestone: '14.9'
type: development
group: group::package
-default_enabled: false
+default_enabled: true
diff --git a/db/migrate/20220217113058_add_status_to_status_check_responses.rb b/db/migrate/20220217113058_add_status_to_status_check_responses.rb
new file mode 100644
index 00000000000..2f118677883
--- /dev/null
+++ b/db/migrate/20220217113058_add_status_to_status_check_responses.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddStatusToStatusCheckResponses < Gitlab::Database::Migration[1.0]
+ def change
+ add_column :status_check_responses, :status, :integer, default: 0, null: false, limit: 2
+ end
+end
diff --git a/db/schema_migrations/20220217113058 b/db/schema_migrations/20220217113058
new file mode 100644
index 00000000000..ef801a0e269
--- /dev/null
+++ b/db/schema_migrations/20220217113058
@@ -0,0 +1 @@
+d2d236e9ee5fa6e9c1ee97431543e871b78e469b812444bd9386dfecf849947b \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index fa57085e398..2f07e478c41 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -20660,7 +20660,8 @@ CREATE TABLE status_check_responses (
merge_request_id bigint NOT NULL,
external_approval_rule_id bigint,
sha bytea NOT NULL,
- external_status_check_id bigint NOT NULL
+ external_status_check_id bigint NOT NULL,
+ status smallint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE status_check_responses_id_seq
diff --git a/doc/api/index.md b/doc/api/index.md
index 6cdf64a01af..40b3fd4e22a 100644
--- a/doc/api/index.md
+++ b/doc/api/index.md
@@ -8,6 +8,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Use the GitLab APIs to automate GitLab.
+NOTE:
+We've launched our first API user survey! Do you use GitLab APIs? How can we improve? [Contribute your feedback today](https://gitlab.fra1.qualtrics.com/jfe/form/SV_cD9wcDYMcVDruSy) and make sure your voice is heard!
+
## REST API
A REST API is available in GitLab.
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 445940e02fc..83631c70f8a 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -399,10 +399,11 @@ Parameters:
| Attribute | Type | Required | Description |
|---------------------|----------------|----------|------------------------------------------------------------------------------------------------------------------------------|
-| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) |
-| `merge_request_iid` | integer | yes | The IID of a project merge request |
-| `body` | string | yes | The content of a note. Limited to 1,000,000 characters. |
-| `created_at` | string | no | Date time string, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a project merge request |
+| `body` | string | yes | The content of a note. Limited to 1,000,000 characters. |
+| `created_at` | string | no | Date time string, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
+| `merge_request_diff_sha`| string | no | The SHA of the head commit which is used to ensure that the merge request hasn't been updated since the API request was sent. This is required for the /merge quick action |
### Modify existing merge request note
diff --git a/doc/api/status_checks.md b/doc/api/status_checks.md
index a3a342854dd..1a41454e621 100644
--- a/doc/api/status_checks.md
+++ b/doc/api/status_checks.md
@@ -44,6 +44,9 @@ GET /projects/:id/merge_requests/:merge_request_iid/status_checks
## Set status of an external status check
+> - Introduced in GitLab 14.9, `passed` status to pass external status checks.
+> - `pass` status to pass checks is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/339039) in GitLab 14.9. Replaced with `passed`.
+
For a single merge request, use the API to inform GitLab that a merge request has passed a check by an external service.
To set the status of an external check, the personal access token used must belong to a user with at least the developer role on the target project of the merge request.
@@ -55,13 +58,14 @@ POST /projects/:id/merge_requests/:merge_request_iid/status_check_responses
**Parameters:**
-| Attribute | Type | Required | Description |
-| -------------------------- | ------- | -------- | ------------------------------------- |
-| `id` | integer | yes | ID of a project |
-| `merge_request_iid` | integer | yes | IID of a merge request |
-| `sha` | string | yes | SHA at `HEAD` of the source branch |
-| `external_status_check_id` | integer | yes | ID of an external status check |
-| `status` | string | no | Set to `pass` to pass the check |
+| Attribute | Type | Required | Description |
+| -------------------------- | ------- | -------- | ---------------------------------------------------------------------------- |
+| `id` | integer | yes | ID of a project |
+| `merge_request_iid` | integer | yes | IID of a merge request |
+| `sha` | string | yes | SHA at `HEAD` of the source branch |
+| `external_status_check_id` | integer | yes | ID of an external status check |
+| `status` | string | no | Set to `passed` to pass the check or `failed` to fail it (GitLab 14.9 and later with feature flag enabled) |
+| `status` | string | no | Set to `pass` to pass the check (GitLab 14.0 to GitLab 14.8) |
NOTE:
`sha` must be the SHA at the `HEAD` of the merge request's source branch.
diff --git a/doc/ci/environments/deployment_approvals.md b/doc/ci/environments/deployment_approvals.md
index 8e56cc1ddd5..12090443e61 100644
--- a/doc/ci/environments/deployment_approvals.md
+++ b/doc/ci/environments/deployment_approvals.md
@@ -79,17 +79,25 @@ Maintainer role.
## Approve or reject a deployment
-NOTE:
-This functionality is currently only available through the API. UI is planned for the near future. See [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/342180/).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342180/) in GitLab 14.9
+
+A blocked deployment is enqueued as soon as it receives the required number of approvals. A single
+rejection causes the deployment to fail. The creator of a deployment cannot approve it, even if they
+have permission to deploy.
+
+You can approve or reject a deployment to a protected environment either in the UI or using the API:
-A blocked deployment is enqueued as soon as it receives the required number of approvals. A single rejection causes the deployment to fail. The creator of a deployment cannot approve it, even if they have permission to deploy.
+### Using the UI
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Deployments > Environments**.
+1. In the deployment's row, select **Approval options** (**{thumb-up}**).
+1. Select **Approve** or **Reject**.
-There are two ways to approve or reject a deployment to a protected environment:
+### Using the API
-1. Using the [UI](index.md#view-environments-and-deployments):
- 1. Select **Approval options** (**{thumb-up}**)
- 1. Select **Approve** or **Reject**
-1. Using the [Deployments API](../../api/deployments.md#approve-or-reject-a-blocked-deployment), users who are allowed to deploy to the protected environment can approve or reject a blocked deployment.
+Users who are allowed to deploy to the protected environment can approve or reject a blocked
+deployment using the [Deployments API](../../api/deployments.md#approve-or-reject-a-blocked-deployment).
Example:
@@ -98,16 +106,16 @@ curl --data "status=approved&comment=Looks good to me" \
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/deployments/1/approval"
```
-### How to see blocked deployments
+## How to see blocked deployments
-#### Using the UI
+### Using the UI
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Deployments > Environments**.
1. Select the environment being deployed to.
1. Look for the `blocked` label.
-#### Using the API
+### Using the API
Use the [Deployments API](../../api/deployments.md) to see deployments.
diff --git a/doc/ci/pipelines/merge_request_pipelines.md b/doc/ci/pipelines/merge_request_pipelines.md
index dcc3e7e6919..d80b745e6bc 100644
--- a/doc/ci/pipelines/merge_request_pipelines.md
+++ b/doc/ci/pipelines/merge_request_pipelines.md
@@ -41,8 +41,10 @@ Both of these types of pipelines can appear on the **Pipelines** tab of a merge
The three types of merge request pipelines are:
- Merge request pipelines, which run on the changes in the merge request's
- source branch. These pipelines display a `detached` label to indicate that the
+ source branch. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352939)
+ in GitLab 14.9, these pipelines display a `merge request` label to indicate that the
pipeline ran only on the contents of the source branch, ignoring the target branch.
+ In GitLab 14.8 and earlier, the label is `detached`.
- [Merged results pipelines](merged_results_pipelines.md), which run on
the result of combining the source branch's changes with the target branch.
- [Merge trains](merge_trains.md), which run when merging multiple merge requests
diff --git a/doc/ci/pipelines/merged_results_pipelines.md b/doc/ci/pipelines/merged_results_pipelines.md
index 4794107cc87..7df9ea3f72f 100644
--- a/doc/ci/pipelines/merged_results_pipelines.md
+++ b/doc/ci/pipelines/merged_results_pipelines.md
@@ -24,7 +24,7 @@ Merged results pipelines can't run when:
- The merge request is a [**Draft** merge request](../../user/project/merge_requests/drafts.md).
In these cases, the pipeline runs as a [merge request pipeline](merge_request_pipelines.md)
-and is labeled as `detached`.
+and [is labeled as `merge request`](merge_request_pipelines.md#types-of-merge-request-pipelines).
## Prerequisites
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 57663efc9f9..ec913df8e4a 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -74,19 +74,27 @@ It picks reviewers and maintainers from the list at the
page, with these behaviors:
1. It doesn't pick people whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status):
- - contains the string 'OOO', 'PTO', 'Parental Leave', or 'Friends and Family'
- - emoji is `:palm_tree:`, `:beach:`, `:beach_umbrella:`, `:beach_with_umbrella:`, `:ferris_wheel:`, `:thermometer:`, `:face_with_thermometer:`, `:red_circle:`, `:bulb:`, `:sun_with_face:`.
- - GitLab user busy indicator is set to true
+ - Contains the string 'OOO', 'PTO', 'Parental Leave', or 'Friends and Family'.
+ - GitLab user **Busy** indicator is set to `True`.
+ - Emoji is any of:
+ - 🌴 `:palm_tree:`
+ - 🏖️ `:beach:`, `:beach_umbrella:`, or `:beach_with_umbrella:`
+ - 🎡 `:ferris_wheel:`
+ - 🌡️ `:thermometer:`
+ - 🤒 `:face_with_thermometer:`
+ - 🔴 `:red_circle:`
+ - 💡 `:bulb:`
+ - 🌞 `:sun_with_face:`
1. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer)
are three times as likely to be picked as other reviewers.
1. Team members whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status) emoji
is 🔵 `:large_blue_circle:` are more likely to be picked. This applies to both reviewers and trainee maintainers.
- - Reviewers with `:large_blue_circle:` are two times as likely to be picked as other reviewers.
- - Trainee maintainers with `:large_blue_circle:` are four times as likely to be picked as other reviewers.
+ - Reviewers with 🔵 `:large_blue_circle:` are two times as likely to be picked as other reviewers.
+ - Trainee maintainers with 🔵 `:large_blue_circle:` are four times as likely to be picked as other reviewers.
1. People whose [GitLab status](../user/profile/index.md#set-your-current-status) emoji
- is 🔶 `:large_orange_diamond:` are half as likely to be picked. This applies to both reviewers and trainee maintainers.
+ is 🔶 `:large_orange_diamond:` or 🔸 `:small_orange_diamond:` are half as likely to be picked. This applies to both reviewers and trainee maintainers.
1. It always picks the same reviewers and maintainers for the same
- branch name (unless their OOO status changes, as in point 1). It
+ branch name (unless their out-of-office (OOO) status changes, as in point 1). It
removes leading `ce-` and `ee-`, and trailing `-ce` and `-ee`, so
that it can be stable for backport branches.
diff --git a/doc/development/product_qualified_lead_guide/index.md b/doc/development/product_qualified_lead_guide/index.md
index 57fd8e70dd0..ceb2b9b2933 100644
--- a/doc/development/product_qualified_lead_guide/index.md
+++ b/doc/development/product_qualified_lead_guide/index.md
@@ -92,6 +92,36 @@ The flow of a PQL lead is as follows:
1. Marketo does scoring and sends the form to Salesforce.
1. Our Sales team uses Salesforce to connect to the leads.
+### Lead flow on GitLab.com
+
+```mermaid
+sequenceDiagram
+ HandRaiseForm Vue Component->>TrialsController#create_hand_raise_lead: GitLab.com frontend sends [lead] to backend
+ TrialsController#create_hand_raise_lead->>CreateHandRaiseLeadService: [lead]
+ CreateHandRaiseLeadService->>SubscriptionPortalClient: [lead]
+ SubscriptionPortalClient->>CustomersDot|TrialsController#create_hand_raise_lead: GitLab.com sends [lead] to CustomersDot
+```
+
+### Lead flow on CustomersDot and later
+
+```mermaid
+sequenceDiagram
+ CustomersDot|TrialsController#create_hand_raise_lead->>PlatypusLogLeadService: Save [lead] to leads table for monitoring purposes
+ CustomersDot|TrialsController#create_hand_raise_lead->>Platypus|CreateLeadWorker: Async worker to submit [lead] to Platypus
+ Platypus|CreateLeadWorker->>Platypus|CreateLeadService: [lead]
+ Platypus|CreateLeadService->>PlatypusApp#post: [lead]
+ PlatypusApp#post->>Platypus: [lead] is sent to Platypus
+```
+
+### Lead flow after Platypus
+
+```mermaid
+sequenceDiagram
+ Platypus->>Workato: [lead]
+ Workato->>Marketo: [lead]
+ Marketo->>Salesforce(SFDC): [lead]
+```
+
## Monitor and manually test leads
- Check the application and Sidekiq logs on `gitlab.com` and CustomersDot to monitor leads.
diff --git a/doc/install/docker.md b/doc/install/docker.md
index ed5e1dda5d5..8223e05e3df 100644
--- a/doc/install/docker.md
+++ b/doc/install/docker.md
@@ -513,6 +513,22 @@ To update GitLab that was [installed using Docker Compose](#install-gitlab-using
If you have used [tags](#use-tagged-versions-of-gitlab) instead, you'll need
to first edit `docker-compose.yml`.
+### Convert Community Edition to Enterprise Edition
+
+You can convert an existing Docker-based GitLab Community Edition (CE) container
+to a GitLab [Enterprise Edition](https://about.gitlab.com/pricing/) (EE) container
+using the same approach as [updating the version](#update).
+
+We recommend you convert from the same version of CE to EE (for example, CE 14.1 to EE 14.1).
+This is not explicitly necessary, and any standard upgrade (for example, CE 14.0 to EE 14.1) should work.
+The following steps assume that you are upgrading the same version.
+
+1. Take a [backup](#back-up-gitlab).
+1. Stop the current CE container, and remove or rename it.
+1. To create a new container with GitLab EE,
+ replace `ce` with `ee` in your `docker run` command or `docker-compose.yml` file.
+ However, reuse the CE container name, port and file mappings, and version.
+
## Back up GitLab
You can create a GitLab backup with:
diff --git a/doc/update/index.md b/doc/update/index.md
index 50f46d38191..5a00a728535 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -250,6 +250,8 @@ To address the above two scenario's, it is advised to do the following prior to
1. Pause your runners.
1. Wait until all jobs are finished.
1. Upgrade GitLab.
+1. [Update GitLab Runner](https://docs.gitlab.com/runner/install/index.html) to the same version
+ as your GitLab version. Both versions [should be the same](https://docs.gitlab.com/runner/#gitlab-runner-versions).
## Checking for pending Advanced Search migrations **(PREMIUM SELF)**
@@ -356,6 +358,8 @@ Edition, follow the guides below based on the installation method:
script, start the application and check its status.
- [Omnibus CE to EE](package/convert_to_ee.md) - Follow this guide to update your Omnibus
GitLab Community Edition to the Enterprise Edition.
+- [Docker CE to EE](../install/docker.md#convert-community-edition-to-enterprise-edition) -
+ Follow this guide to update your GitLab Community Edition container to an Enterprise Edition container.
### Enterprise to Community Edition
@@ -456,7 +460,7 @@ or [init scripts](upgrading_from_source.md#configure-sysv-init-script) by [follo
This is because we [enabled `paginated_tree_graphql_query by default in 14.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70913/diffs), so if GitLab UI is on 14.4 and its API is on 14.3, the frontend will have this feature enabled but the backend will have it disabled. This will result in the following error:
```shell
- bundle.esm.js:63 Uncaught (in promise) Error: GraphQL error: Field 'paginatedTree' doesn't exist on type 'Repository'
+ bundle.esm.js:63 Uncaught (in promise) Error: GraphQL error: Field 'paginatedTree' doesn't exist on type 'Repository'
```
### 14.4.0
diff --git a/lib/api/ci/secure_files.rb b/lib/api/ci/secure_files.rb
index 715a8b37fae..d5b21e2ef29 100644
--- a/lib/api/ci/secure_files.rb
+++ b/lib/api/ci/secure_files.rb
@@ -7,8 +7,8 @@ module API
before do
authenticate!
- authorize! :admin_build, user_project
feature_flag_enabled?
+ authorize! :read_secure_files, user_project
end
feature_category :pipeline_authoring
@@ -52,39 +52,44 @@ module API
body secure_file.file.read
end
- desc 'Upload a Secure File'
- params do
- requires :name, type: String, desc: 'The name of the file'
- requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The secure file to be uploaded'
- optional :permissions, type: String, desc: 'The file permissions', default: 'read_only', values: %w[read_only read_write execute]
- end
-
- route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
- post ':id/secure_files' do
- secure_file = user_project.secure_files.new(
- name: params[:name],
- permissions: params[:permissions] || :read_only
- )
-
- secure_file.file = params[:file]
-
- file_too_large! unless secure_file.file.size < ::Ci::SecureFile::FILE_SIZE_LIMIT.to_i
+ resource do
+ before do
+ authorize! :admin_secure_files, user_project
+ end
- if secure_file.save
- present secure_file, with: Entities::Ci::SecureFile
- else
- render_validation_error!(secure_file)
+ desc 'Upload a Secure File'
+ params do
+ requires :name, type: String, desc: 'The name of the file'
+ requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The secure file to be uploaded'
+ optional :permissions, type: String, desc: 'The file permissions', default: 'read_only', values: %w[read_only read_write execute]
+ end
+ route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
+ post ':id/secure_files' do
+ secure_file = user_project.secure_files.new(
+ name: params[:name],
+ permissions: params[:permissions] || :read_only
+ )
+
+ secure_file.file = params[:file]
+
+ file_too_large! unless secure_file.file.size < ::Ci::SecureFile::FILE_SIZE_LIMIT.to_i
+
+ if secure_file.save
+ present secure_file, with: Entities::Ci::SecureFile
+ else
+ render_validation_error!(secure_file)
+ end
end
- end
- desc 'Delete an individual Secure File'
- route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
- delete ':id/secure_files/:secure_file_id' do
- secure_file = user_project.secure_files.find(params[:secure_file_id])
+ desc 'Delete an individual Secure File'
+ route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: true
+ delete ':id/secure_files/:secure_file_id' do
+ secure_file = user_project.secure_files.find(params[:secure_file_id])
- secure_file.destroy!
+ ::Ci::DestroySecureFileService.new(user_project, current_user).execute(secure_file)
- no_content!
+ no_content!
+ end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 93ef77d5a62..b260f5289b3 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -75,6 +75,7 @@ module API
requires :body, type: String, desc: 'The content of a note'
optional :confidential, type: Boolean, desc: 'Confidentiality note flag, default is false'
optional :created_at, type: String, desc: 'The creation date of the note'
+ optional :merge_request_diff_head_sha, type: String, desc: 'The SHA of the head commit'
end
post ":id/#{noteables_str}/:noteable_id/notes", feature_category: feature_category do
allowlist =
@@ -87,7 +88,8 @@ module API
noteable_type: noteables_str.classify,
noteable_id: noteable.id,
confidential: params[:confidential],
- created_at: params[:created_at]
+ created_at: params[:created_at],
+ merge_request_diff_head_sha: params[:merge_request_diff_head_sha]
}
note = create_note(noteable, opts)
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 16d889e556d..4b2250d089d 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -152,7 +152,7 @@ module ContainerRegistry
@faraday_blob ||= faraday_base do |conn|
initialize_connection(conn, @options)
- if Feature.enabled?(:container_registry_follow_redirects_middleware)
+ if Feature.enabled?(:container_registry_follow_redirects_middleware, default_enabled: :yaml)
conn.use ::FaradayMiddleware::FollowRedirects, REDIRECT_OPTIONS
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index dc24020f470..2f7d3e03bb9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -847,9 +847,6 @@ msgstr ""
msgid "%{name}, confirm your email address now!"
msgstr ""
-msgid "%{name}: %{resultsString}"
-msgstr ""
-
msgid "%{no_of_days} day"
msgid_plural "%{no_of_days} days"
msgstr[0] ""
@@ -1280,7 +1277,7 @@ msgstr ""
msgid "+%{tags} more"
msgstr ""
-msgid ", and "
+msgid ", "
msgstr ""
msgid ", or "
@@ -10917,6 +10914,15 @@ msgstr ""
msgid "CycleAnalytics|Average time to completion"
msgstr ""
+msgid "CycleAnalytics|Create a custom value stream to view metrics about stages specific to your development process. Use your value stream to visualize your DevSecOps lifecycle, determine the velocity of your group, and identify inefficient processes."
+msgstr ""
+
+msgid "CycleAnalytics|Create a custom value stream…"
+msgstr ""
+
+msgid "CycleAnalytics|Custom value streams to measure your DevSecOps lifecycle"
+msgstr ""
+
msgid "CycleAnalytics|Date"
msgstr ""
@@ -30953,9 +30959,6 @@ msgstr ""
msgid "Reports|Filename"
msgstr ""
-msgid "Reports|Full report"
-msgstr ""
-
msgid "Reports|Head report parsing error:"
msgstr ""
@@ -30998,15 +31001,9 @@ msgstr ""
msgid "Reports|Test summary failed loading results"
msgstr ""
-msgid "Reports|Test summary failed to load results"
-msgstr ""
-
msgid "Reports|Test summary results are being parsed"
msgstr ""
-msgid "Reports|Test summary results are loading"
-msgstr ""
-
msgid "Reports|Tool"
msgstr ""
@@ -35193,6 +35190,9 @@ msgstr ""
msgid "Status: %{title}"
msgstr ""
+msgid "StatusCheck| %{failed} failed, and %{pending} pending"
+msgstr ""
+
msgid "StatusCheck|%{failed} failed"
msgstr ""
@@ -35265,9 +35265,6 @@ msgstr ""
msgid "StatusCheck|Update status check"
msgstr ""
-msgid "StatusCheck|When this merge request is updated, a call is sent to the following APIs to confirm their status. %{linkStart}Learn more%{linkEnd}."
-msgstr ""
-
msgid "StatusCheck|You are about to remove the %{name} status check."
msgstr ""
@@ -41030,6 +41027,9 @@ msgstr ""
msgid "Vulnerability|Class"
msgstr ""
+msgid "Vulnerability|Cluster"
+msgstr ""
+
msgid "Vulnerability|Code Review"
msgstr ""
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index 97b59a09518..0822b293f75 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -27,6 +27,7 @@ describe('Pipelines filtered search', () => {
wrapper = mount(PipelinesFilteredSearch, {
propsData: {
projectId: '21',
+ defaultBranchName: 'main',
params,
},
attachTo: document.body,
@@ -69,6 +70,7 @@ describe('Pipelines filtered search', () => {
title: 'Branch name',
unique: true,
projectId: '21',
+ defaultBranchName: 'main',
operators: OPERATOR_IS_ONLY,
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index ca19a6d337a..20ed12cd1f5 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -27,6 +27,7 @@ jest.mock('~/flash');
const mockProjectPath = 'twitter/flight';
const mockProjectId = '21';
+const mockDefaultBranchName = 'main';
const mockPipelinesEndpoint = `/${mockProjectPath}/pipelines.json`;
const mockPipelinesIds = mockPipelinesResponse.pipelines.map(({ id }) => id);
const mockPipelineWithStages = mockPipelinesResponse.pipelines.find(
@@ -85,6 +86,7 @@ describe('Pipelines', () => {
propsData: {
store: new Store(),
projectId: mockProjectId,
+ defaultBranchName: mockDefaultBranchName,
endpoint: mockPipelinesEndpoint,
params: {},
...props,
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
index 2e44f40eda4..42ae154fb5e 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -1,5 +1,7 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelineBranchNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue';
import { branches, mockBranchesAfterMap } from '../mock_data';
@@ -10,6 +12,8 @@ describe('Pipeline Branch Name Token', () => {
const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const getBranchSuggestions = () =>
+ findAllFilteredSearchSuggestions().wrappers.map((w) => w.text());
const stubs = {
GlFilteredSearchToken: {
@@ -24,6 +28,7 @@ describe('Pipeline Branch Name Token', () => {
title: 'Branch name',
unique: true,
projectId: '21',
+ defaultBranchName: null,
disabled: false,
},
value: {
@@ -31,6 +36,19 @@ describe('Pipeline Branch Name Token', () => {
},
};
+ const optionsWithDefaultBranchName = (options) => {
+ return {
+ propsData: {
+ ...defaultProps,
+ config: {
+ ...defaultProps.config,
+ defaultBranchName: 'main',
+ },
+ },
+ ...options,
+ };
+ };
+
const createComponent = (options, data) => {
wrapper = shallowMount(PipelineBranchNameToken, {
propsData: {
@@ -94,5 +112,34 @@ describe('Pipeline Branch Name Token', () => {
expect(findAllFilteredSearchSuggestions()).toHaveLength(mockBranches.length);
});
+
+ it('shows the default branch first if no branch was searched for', async () => {
+ const mockBranches = [{ name: 'branch-1' }];
+ jest.spyOn(Api, 'branches').mockResolvedValue({ data: mockBranches });
+
+ createComponent(optionsWithDefaultBranchName({ stubs }), { loading: false });
+ await nextTick();
+ expect(getBranchSuggestions()).toEqual(['main', 'branch-1']);
+ });
+
+ it('does not show the default branch if a search term was provided', async () => {
+ const mockBranches = [{ name: 'branch-1' }];
+ jest.spyOn(Api, 'branches').mockResolvedValue({ data: mockBranches });
+
+ createComponent(optionsWithDefaultBranchName(), { loading: false });
+
+ findFilteredSearchToken().vm.$emit('input', { data: 'branch-1' });
+ await waitForPromises();
+ expect(getBranchSuggestions()).toEqual(['branch-1']);
+ });
+
+ it('shows the default branch only once if it appears in the results', async () => {
+ const mockBranches = [{ name: 'main' }];
+ jest.spyOn(Api, 'branches').mockResolvedValue({ data: mockBranches });
+
+ createComponent(optionsWithDefaultBranchName({ stubs }), { loading: false });
+ await nextTick();
+ expect(getBranchSuggestions()).toEqual(['main']);
+ });
});
});
diff --git a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
deleted file mode 100644
index 472dbc104ce..00000000000
--- a/spec/frontend/vue_mr_widget/extensions/test_report/index_spec.js
+++ /dev/null
@@ -1,149 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import testReportExtension from '~/vue_merge_request_widget/extensions/test_report';
-import { i18n } from '~/vue_merge_request_widget/extensions/test_report/constants';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { trimText } from 'helpers/text_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
-import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
-import httpStatusCodes from '~/lib/utils/http_status';
-
-import { failedReport } from '../../../reports/mock_data/mock_data';
-import mixedResultsTestReports from '../../../reports/mock_data/new_and_fixed_failures_report.json';
-import newErrorsTestReports from '../../../reports/mock_data/new_errors_report.json';
-import newFailedTestReports from '../../../reports/mock_data/new_failures_report.json';
-import successTestReports from '../../../reports/mock_data/no_failures_report.json';
-import resolvedFailures from '../../../reports/mock_data/resolved_failures.json';
-
-const reportWithParsingErrors = failedReport;
-reportWithParsingErrors.suites[0].suite_errors = {
- head: 'JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
- base: 'JUnit data parsing failed: string not matched',
-};
-
-describe('Test report extension', () => {
- let wrapper;
- let mock;
-
- registerExtension(testReportExtension);
-
- const endpoint = '/root/repo/-/merge_requests/4/test_reports.json';
-
- const mockApi = (statusCode, data = mixedResultsTestReports) => {
- mock.onGet(endpoint).reply(statusCode, data);
- };
-
- const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
- const findTertiaryButton = () => wrapper.find(GlButton);
- const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
-
- const createComponent = () => {
- wrapper = mountExtended(extensionsContainer, {
- propsData: {
- mr: {
- testResultsPath: endpoint,
- headBlobPath: 'head/blob/path',
- pipeline: { path: 'pipeline/path' },
- },
- },
- });
- };
-
- const createExpandedWidgetWithData = async (data = mixedResultsTestReports) => {
- mockApi(httpStatusCodes.OK, data);
- createComponent();
- await waitForPromises();
- findToggleCollapsedButton().trigger('click');
- await waitForPromises();
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- });
-
- describe('summary', () => {
- it('displays loading text', () => {
- mockApi(httpStatusCodes.OK);
- createComponent();
-
- expect(wrapper.text()).toContain(i18n.loading);
- });
-
- it('displays failed loading text', async () => {
- mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
- createComponent();
-
- await waitForPromises();
-
- expect(wrapper.text()).toContain(i18n.error);
- });
-
- it.each`
- description | mockData | expectedResult
- ${'mixed test results'} | ${mixedResultsTestReports} | ${'Test summary: 2 failed and 2 fixed test results, 11 total tests'}
- ${'unchanged test results'} | ${successTestReports} | ${'Test summary: no changed test results, 11 total tests'}
- ${'tests with errors'} | ${newErrorsTestReports} | ${'Test summary: 2 errors, 11 total tests'}
- ${'failed test results'} | ${newFailedTestReports} | ${'Test summary: 2 failed, 11 total tests'}
- ${'resolved failures'} | ${resolvedFailures} | ${'Test summary: 4 fixed test results, 11 total tests'}
- `('displays summary text for $description', async ({ mockData, expectedResult }) => {
- mockApi(httpStatusCodes.OK, mockData);
- createComponent();
-
- await waitForPromises();
-
- expect(wrapper.text()).toContain(expectedResult);
- });
-
- it('displays a link to the full report', async () => {
- mockApi(httpStatusCodes.OK);
- createComponent();
-
- await waitForPromises();
-
- expect(findTertiaryButton().text()).toBe('Full report');
- expect(findTertiaryButton().attributes('href')).toBe('pipeline/path/test_report');
- });
-
- it('shows an error when a suite has a parsing error', async () => {
- mockApi(httpStatusCodes.OK, reportWithParsingErrors);
- createComponent();
-
- await waitForPromises();
-
- expect(wrapper.text()).toContain(i18n.error);
- });
- });
-
- describe('expanded data', () => {
- it('displays summary for each suite', async () => {
- await createExpandedWidgetWithData();
-
- expect(trimText(findAllExtensionListItems().at(0).text())).toBe(
- 'rspec:pg: 1 failed and 2 fixed test results, 8 total tests',
- );
- expect(trimText(findAllExtensionListItems().at(1).text())).toBe(
- 'java ant: 1 failed, 3 total tests',
- );
- });
-
- it('displays suite parsing errors', async () => {
- await createExpandedWidgetWithData(reportWithParsingErrors);
-
- const suiteText = trimText(findAllExtensionListItems().at(0).text());
-
- expect(suiteText).toContain(
- 'Head report parsing error: JUnit XML parsing failed: 2:24: FATAL: attributes construct error',
- );
- expect(suiteText).toContain(
- 'Base report parsing error: JUnit data parsing failed: string not matched',
- );
- });
- });
-});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 9a21389dbb4..0540107ea5f 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -1025,7 +1025,7 @@ describe('MrWidgetOptions', () => {
it('captures sentry error and displays error when poll has failed', () => {
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
- expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
});
});
});
@@ -1036,7 +1036,7 @@ describe('MrWidgetOptions', () => {
const itHandlesTheException = () => {
expect(captureException).toHaveBeenCalledTimes(1);
expect(captureException).toHaveBeenCalledWith(new Error('Fetch error'));
- expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('failed');
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe('error');
};
beforeEach(() => {
diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb
index a7a96efcb3f..2b76eaa87bc 100644
--- a/spec/helpers/ci/pipelines_helper_spec.rb
+++ b/spec/helpers/ci/pipelines_helper_spec.rb
@@ -106,6 +106,7 @@ RSpec.describe Ci::PipelinesHelper do
it 'has the expected keys' do
expect(subject.keys).to match_array([:endpoint,
:project_id,
+ :default_branch_name,
:params,
:artifacts_endpoint,
:artifacts_endpoint_placeholder,
diff --git a/spec/requests/api/ci/secure_files_spec.rb b/spec/requests/api/ci/secure_files_spec.rb
index c1700bf5760..aa479cb8713 100644
--- a/spec/requests/api/ci/secure_files_spec.rb
+++ b/spec/requests/api/ci/secure_files_spec.rb
@@ -8,49 +8,72 @@ RSpec.describe API::Ci::SecureFiles do
stub_feature_flags(ci_secure_files: true)
end
- let_it_be(:user) { create(:user) }
- let_it_be(:user2) { create(:user) }
- let_it_be(:project) { create(:project, creator_id: user.id) }
- let_it_be(:maintainer) { create(:project_member, :maintainer, user: user, project: project) }
- let_it_be(:developer) { create(:project_member, :developer, user: user2, project: project) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:anonymous) { create(:user) }
+ let_it_be(:project) { create(:project, creator_id: maintainer.id) }
let_it_be(:secure_file) { create(:ci_secure_file, project: project) }
+ before_all do
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ project.add_guest(guest)
+ end
+
describe 'GET /projects/:id/secure_files' do
context 'feature flag' do
it 'returns a 503 when the feature flag is disabled' do
stub_feature_flags(ci_secure_files: false)
- get api("/projects/#{project.id}/secure_files", user)
+ get api("/projects/#{project.id}/secure_files", maintainer)
expect(response).to have_gitlab_http_status(:service_unavailable)
end
it 'returns a 200 when the feature flag is enabled' do
- get api("/projects/#{project.id}/secure_files", user)
+ get api("/projects/#{project.id}/secure_files", maintainer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a(Array)
+ end
+ end
+
+ context 'authenticated user with admin permissions' do
+ it 'returns project secure files' do
+ get api("/projects/#{project.id}/secure_files", maintainer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a(Array)
end
end
- context 'authorized user with proper permissions' do
+ context 'authenticated user with read permissions' do
it 'returns project secure files' do
- get api("/projects/#{project.id}/secure_files", user)
+ get api("/projects/#{project.id}/secure_files", developer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a(Array)
end
end
- context 'authorized user with invalid permissions' do
+ context 'authenticated user with guest permissions' do
it 'does not return project secure files' do
- get api("/projects/#{project.id}/secure_files", user2)
+ get api("/projects/#{project.id}/secure_files", guest)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
- context 'unauthorized user' do
+ context 'authenticated user with no permissions' do
+ it 'does not return project secure files' do
+ get api("/projects/#{project.id}/secure_files", anonymous)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
it 'does not return project secure files' do
get api("/projects/#{project.id}/secure_files")
@@ -60,9 +83,9 @@ RSpec.describe API::Ci::SecureFiles do
end
describe 'GET /projects/:id/secure_files/:secure_file_id' do
- context 'authorized user with proper permissions' do
+ context 'authenticated user with admin permissions' do
it 'returns project secure file details' do
- get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user)
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}", maintainer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['name']).to eq(secure_file.name)
@@ -70,21 +93,31 @@ RSpec.describe API::Ci::SecureFiles do
end
it 'responds with 404 Not Found if requesting non-existing secure file' do
- get api("/projects/#{project.id}/secure_files/99999", user)
+ get api("/projects/#{project.id}/secure_files/#{non_existing_record_id}", maintainer)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'authorized user with invalid permissions' do
+ context 'authenticated user with read permissions' do
+ it 'returns project secure file details' do
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['name']).to eq(secure_file.name)
+ expect(json_response['permissions']).to eq(secure_file.permissions)
+ end
+ end
+
+ context 'authenticated user with no permissions' do
it 'does not return project secure file details' do
- get api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2)
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}", anonymous)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'unauthorized user' do
+ context 'unauthenticated user' do
it 'does not return project secure file details' do
get api("/projects/#{project.id}/secure_files/#{secure_file.id}")
@@ -94,34 +127,47 @@ RSpec.describe API::Ci::SecureFiles do
end
describe 'GET /projects/:id/secure_files/:secure_file_id/download' do
- context 'authorized user with proper permissions' do
+ context 'authenticated user with admin permissions' do
it 'returns a secure file' do
sample_file = fixture_file('ci_secure_files/upload-keystore.jks')
secure_file.file = CarrierWaveStringFile.new(sample_file)
secure_file.save!
- get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user)
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", maintainer)
expect(response).to have_gitlab_http_status(:ok)
expect(Base64.encode64(response.body)).to eq(Base64.encode64(sample_file))
end
it 'responds with 404 Not Found if requesting non-existing secure file' do
- get api("/projects/#{project.id}/secure_files/99999/download", user)
+ get api("/projects/#{project.id}/secure_files/#{non_existing_record_id}/download", maintainer)
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'authorized user with invalid permissions' do
+ context 'authenticated user with read permissions' do
+ it 'returns a secure file' do
+ sample_file = fixture_file('ci_secure_files/upload-keystore.jks')
+ secure_file.file = CarrierWaveStringFile.new(sample_file)
+ secure_file.save!
+
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", developer)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(Base64.encode64(response.body)).to eq(Base64.encode64(sample_file))
+ end
+ end
+
+ context 'authenticated user with no permissions' do
it 'does not return project secure file details' do
- get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user2)
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", anonymous)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'unauthorized user' do
+ context 'unauthenticated user' do
it 'does not return project secure file details' do
get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download")
@@ -131,7 +177,7 @@ RSpec.describe API::Ci::SecureFiles do
end
describe 'POST /projects/:id/secure_files' do
- context 'authorized user with proper permissions' do
+ context 'authenticated user with admin permissions' do
it 'creates a secure file' do
params = {
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks'),
@@ -140,7 +186,7 @@ RSpec.describe API::Ci::SecureFiles do
}
expect do
- post api("/projects/#{project.id}/secure_files", user), params: params
+ post api("/projects/#{project.id}/secure_files", maintainer), params: params
end.to change {project.secure_files.count}.by(1)
expect(response).to have_gitlab_http_status(:created)
@@ -164,7 +210,7 @@ RSpec.describe API::Ci::SecureFiles do
}
expect do
- post api("/projects/#{project.id}/secure_files", user), params: params
+ post api("/projects/#{project.id}/secure_files", maintainer), params: params
end.to change {project.secure_files.count}.by(1)
expect(json_response['permissions']).to eq('read_only')
@@ -177,11 +223,11 @@ RSpec.describe API::Ci::SecureFiles do
permissions: 'read_write'
}
- post api("/projects/#{project.id}/secure_files", user), params: post_params
+ post api("/projects/#{project.id}/secure_files", maintainer), params: post_params
secure_file_id = json_response['id']
- get api("/projects/#{project.id}/secure_files/#{secure_file_id}/download", user)
+ get api("/projects/#{project.id}/secure_files/#{secure_file_id}/download", maintainer)
expect(Base64.encode64(response.body)).to eq(Base64.encode64(fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks').read))
end
@@ -189,7 +235,9 @@ RSpec.describe API::Ci::SecureFiles do
it 'returns an error when the file checksum fails to validate' do
secure_file.update!(checksum: 'foo')
- get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", user)
+ expect do
+ get api("/projects/#{project.id}/secure_files/#{secure_file.id}/download", maintainer)
+ end.not_to change { project.secure_files.count }
expect(response.code).to eq("500")
end
@@ -199,7 +247,9 @@ RSpec.describe API::Ci::SecureFiles do
name: 'upload-keystore.jks'
}
- post api("/projects/#{project.id}/secure_files", user), params: post_params
+ expect do
+ post api("/projects/#{project.id}/secure_files", maintainer), params: post_params
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('file is missing')
@@ -210,7 +260,9 @@ RSpec.describe API::Ci::SecureFiles do
file: fixture_file_upload('spec/fixtures/ci_secure_files/upload-keystore.jks')
}
- post api("/projects/#{project.id}/secure_files", user), params: post_params
+ expect do
+ post api("/projects/#{project.id}/secure_files", maintainer), params: post_params
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('name is missing')
@@ -223,7 +275,9 @@ RSpec.describe API::Ci::SecureFiles do
permissions: 'foo'
}
- post api("/projects/#{project.id}/secure_files", user), params: post_params
+ expect do
+ post api("/projects/#{project.id}/secure_files", maintainer), params: post_params
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('permissions does not have a valid value')
@@ -241,7 +295,9 @@ RSpec.describe API::Ci::SecureFiles do
name: 'upload-keystore.jks'
}
- post api("/projects/#{project.id}/secure_files", user), params: post_params
+ expect do
+ post api("/projects/#{project.id}/secure_files", maintainer), params: post_params
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -256,23 +312,39 @@ RSpec.describe API::Ci::SecureFiles do
name: 'upload-keystore.jks'
}
- post api("/projects/#{project.id}/secure_files", user), params: post_params
+ expect do
+ post api("/projects/#{project.id}/secure_files", maintainer), params: post_params
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:payload_too_large)
end
end
- context 'authorized user with invalid permissions' do
+ context 'authenticated user with read permissions' do
it 'does not create a secure file' do
- post api("/projects/#{project.id}/secure_files", user2)
+ expect do
+ post api("/projects/#{project.id}/secure_files", developer)
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
- context 'unauthorized user' do
+ context 'authenticated user with no permissions' do
it 'does not create a secure file' do
- post api("/projects/#{project.id}/secure_files")
+ expect do
+ post api("/projects/#{project.id}/secure_files", anonymous)
+ end.not_to change { project.secure_files.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not create a secure file' do
+ expect do
+ post api("/projects/#{project.id}/secure_files")
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -280,33 +352,49 @@ RSpec.describe API::Ci::SecureFiles do
end
describe 'DELETE /projects/:id/secure_files/:secure_file_id' do
- context 'authorized user with proper permissions' do
+ context 'authenticated user with admin permissions' do
it 'deletes the secure file' do
expect do
- delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user)
+ delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", maintainer)
expect(response).to have_gitlab_http_status(:no_content)
- end.to change {project.secure_files.count}.by(-1)
+ end.to change { project.secure_files.count }
end
it 'responds with 404 Not Found if requesting non-existing secure_file' do
- delete api("/projects/#{project.id}/secure_files/99999", user)
+ expect do
+ delete api("/projects/#{project.id}/secure_files/#{non_existing_record_id}", maintainer)
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:not_found)
end
end
- context 'authorized user with invalid permissions' do
+ context 'authenticated user with read permissions' do
it 'does not delete the secure_file' do
- delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", user2)
+ expect do
+ delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", developer)
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
- context 'unauthorized user' do
+ context 'authenticated user with no permissions' do
it 'does not delete the secure_file' do
- delete api("/projects/#{project.id}/secure_files/#{secure_file.id}")
+ expect do
+ delete api("/projects/#{project.id}/secure_files/#{secure_file.id}", anonymous)
+ end.not_to change { project.secure_files.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not delete the secure_file' do
+ expect do
+ delete api("/projects/#{project.id}/secure_files/#{secure_file.id}")
+ end.not_to change { project.secure_files.count }
expect(response).to have_gitlab_http_status(:unauthorized)
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 3c28aed6cac..455400072bf 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -228,44 +228,83 @@ RSpec.describe API::Notes do
end
let(:request_body) { 'Hi!' }
+ let(:params) { { body: request_body } }
let(:request_path) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes" }
- subject { post api(request_path, user), params: { body: request_body } }
+ subject { post api(request_path, user), params: params }
context 'a command only note' do
- let(:request_body) { "/spend 1h" }
+ context '/spend' do
+ let(:request_body) { "/spend 1h" }
- before do
- project.add_developer(user)
- end
+ before do
+ project.add_developer(user)
+ end
- it 'returns 202 Accepted status' do
- subject
+ it 'returns 202 Accepted status' do
+ subject
- expect(response).to have_gitlab_http_status(:accepted)
- end
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
- it 'does not actually create a new note' do
- expect { subject }.not_to change { Note.where(system: false).count }
- end
+ it 'does not actually create a new note' do
+ expect { subject }.not_to change { Note.where(system: false).count }
+ end
- it 'does however create a system note about the change', :sidekiq_inline do
- expect { subject }.to change { Note.system.count }.by(1)
- end
+ it 'does however create a system note about the change', :sidekiq_inline do
+ expect { subject }.to change { Note.system.count }.by(1)
+ end
+
+ it 'applies the commands' do
+ expect { subject }.to change { merge_request.reset.total_time_spent }
+ end
+
+ it 'reports the changes' do
+ subject
- it 'applies the commands' do
- expect { subject }.to change { merge_request.reset.total_time_spent }
+ expect(json_response).to include(
+ 'commands_changes' => include(
+ 'spend_time' => include('duration' => 3600)
+ ),
+ 'summary' => include('Added 1h spent time.')
+ )
+ end
end
- it 'reports the changes' do
- subject
+ context '/merge' do
+ let(:request_body) { "/merge" }
+ let(:project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request_with_multiple_diffs, source_project: project, target_project: project, author: user) }
+ let(:params) { { body: request_body, merge_request_diff_head_sha: merge_request.diff_head_sha } }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns 202 Accepted status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ it 'does not actually create a new note' do
+ expect { subject }.not_to change { Note.where(system: false).count }
+ end
+
+ it 'applies the commands' do
+ expect { subject }.to change { merge_request.reload.merge_jid.present? }.from(false).to(true)
+ end
- expect(json_response).to include(
- 'commands_changes' => include(
- 'spend_time' => include('duration' => 3600)
- ),
- 'summary' => include('Added 1h spent time.')
- )
+ it 'reports the changes' do
+ subject
+
+ expect(json_response).to include(
+ 'commands_changes' => include(
+ 'merge' => merge_request.diff_head_sha
+ ),
+ 'summary' => ['Merged this merge request.']
+ )
+ end
end
end
diff --git a/spec/services/ci/destroy_secure_file_service_spec.rb b/spec/services/ci/destroy_secure_file_service_spec.rb
new file mode 100644
index 00000000000..6a30d33f4ca
--- /dev/null
+++ b/spec/services/ci/destroy_secure_file_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::DestroySecureFileService do
+ let_it_be(:maintainer_user) { create(:user) }
+ let_it_be(:developer_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:secure_file) { create(:ci_secure_file, project: project) }
+ let_it_be(:project_member) { create(:project_member, :maintainer, user: maintainer_user, project: project) }
+ let_it_be(:project_member2) { create(:project_member, :developer, user: developer_user, project: project) }
+
+ subject { described_class.new(project, user).execute(secure_file) }
+
+ context 'user is a maintainer' do
+ let(:user) { maintainer_user }
+
+ it 'destroys the secure file' do
+ subject
+
+ expect { secure_file.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'user is a developer' do
+ let(:user) { developer_user }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index c801e75bf35..cd923720631 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -43,6 +43,7 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
let!(:report_result) { create(:ci_build_report_result, build: build) }
let!(:pending_state) { create(:ci_build_pending_state, build: build) }
let!(:pipeline_artifact) { create(:ci_pipeline_artifact, pipeline: pipeline) }
+ let!(:secure_file) { create(:ci_secure_file, project: project) }
it 'deletes build and pipeline related records' do
expect { destroy_project(project, user, {}) }
@@ -56,6 +57,7 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi
.and change { Ci::BuildReportResult.count }.by(-1)
.and change { Ci::BuildRunnerSession.count }.by(-1)
.and change { Ci::Pipeline.count }.by(-1)
+ .and change { Ci::SecureFile.count }.by(-1)
end
it 'avoids N+1 queries' do