From 2d8454515e7b631a8f39a6415c86154d6c62841c Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 16 Jul 2020 15:09:38 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .gitlab-ci.yml | 2 + .gitlab/ci/reports.gitlab-ci.yml | 1 + .gitlab/ci/setup.gitlab-ci.yml | 1 + GITLAB_WORKHORSE_VERSION | 2 +- .../javascripts/import_projects/store/actions.js | 15 +- .../incidents_settings/components/alerts_form.vue | 25 +-- .../components/incidents_settings_tabs.vue | 14 +- .../components/pagerduty_form.vue | 183 +++++++++++++++++++++ .../javascripts/incidents_settings/constants.js | 38 ++++- .../incidents_settings_service.js | 32 ++++ app/assets/javascripts/incidents_settings/index.js | 19 ++- .../pipelines/components/dag/dag_annotations.vue | 4 +- .../pipelines/components/graph/graph_component.vue | 14 +- .../pipelines/components/graph/job_item.vue | 14 +- .../pipelines/components/graph/linked_pipeline.vue | 52 +++--- .../components/graph/linked_pipelines_column.vue | 4 + .../components/graph/stage_column_component.vue | 6 + .../components/test_reports/test_suite_table.vue | 4 +- .../components/test_reports/test_summary.vue | 2 +- .../components/test_reports/test_summary_table.vue | 4 +- .../javascripts/reports/components/summary_row.vue | 7 +- .../components/design_management/design.scss | 2 + app/assets/stylesheets/pages/pipelines.scss | 4 - app/assets/stylesheets/utilities.scss | 4 + app/models/concerns/approvable_base.rb | 2 +- app/models/member.rb | 9 + app/models/members/group_member.rb | 9 +- app/models/namespace.rb | 1 + app/models/namespace_setting.rb | 9 + app/models/user.rb | 3 +- app/services/groups/create_service.rb | 10 +- app/services/members/create_service.rb | 2 +- .../merge_requests/remove_approval_service.rb | 2 +- .../196066-add-milestone-expired-info-be-2.yml | 5 + .../unreleased/217392-update-workhorse-version.yml | 5 + .../224039-jira-issues-integration-doc.yml | 6 + .../unreleased/add-namespace-settings-table.yml | 5 + changelogs/unreleased/dag-annotations-sticky.yml | 5 + changelogs/unreleased/downstream-pipeline-ux.yml | 5 + .../unreleased/fix-design-note-border-radius.yml | 5 + .../remove-ff-in-count-users-by-group.yml | 5 + .../unreleased/xanf-expose-bitbucket-error.yml | 5 + .../20200703124823_create_namespace_settings.rb | 22 +++ .../20200703125016_backfill_namespace_settings.rb | 29 ++++ db/structure.sql | 14 ++ doc/api/milestones.md | 15 +- doc/development/testing_guide/review_apps.md | 25 +-- .../application_security/configuration/index.md | 11 +- .../img/container_scanning_v13_1.png | Bin 31586 -> 0 bytes .../img/container_scanning_v13_2.png | Bin 0 -> 8658 bytes .../container_scanning/index.md | 2 +- .../dast/img/dast_all_v13_1.png | Bin 29403 -> 0 bytes .../application_security/dast/img/dast_v13_2.png | Bin 0 -> 6763 bytes doc/user/application_security/dast/index.md | 2 +- .../img/dependency_scanning_v13_1.png | Bin 38992 -> 0 bytes .../img/dependency_scanning_v13_2.png | Bin 0 -> 10289 bytes .../dependency_scanning/index.md | 2 +- .../application_security/sast/img/sast_v13_1.png | Bin 39325 -> 0 bytes .../application_security/sast/img/sast_v13_2.png | Bin 0 -> 7703 bytes doc/user/application_security/sast/index.md | 2 +- .../img/secret-detection-merge-request-ui.png | Bin 100409 -> 0 bytes .../img/secret_detection_v13_2.png | Bin 0 -> 5863 bytes .../application_security/secret_detection/index.md | 2 +- .../img/compliance_dashboard_v12_10.png | Bin 98355 -> 0 bytes .../img/compliance_dashboard_v13_2.png | Bin 0 -> 84922 bytes doc/user/compliance/compliance_dashboard/index.md | 3 +- doc/user/group/iterations/index.md | 18 +- .../img/jira/open_jira_issues_list_v13.2.png | Bin 0 -> 130755 bytes doc/user/project/integrations/jira.md | 97 ++++++++--- lib/api/entities/merge_request_approvals.rb | 4 +- lib/api/members.rb | 2 +- lib/api/milestone_responses.rb | 29 +++- lib/api/project_milestones.rb | 2 + .../backfill_namespace_settings.rb | 18 ++ lib/gitlab/danger/helper.rb | 3 +- locale/gitlab.pot | 84 +++++++--- scripts/prepare_build.sh | 2 +- .../projects/project_members_controller_spec.rb | 23 +++ spec/features/projects/pipelines/pipeline_spec.rb | 4 +- .../frontend/import_projects/store/actions_spec.js | 27 ++- .../incidents_settings_tabs_spec.js.snap | 9 +- .../__snapshots__/pagerduty_form_spec.js.snap | 89 ++++++++++ .../components/alerts_form_spec.js | 44 ++--- .../components/incidents_settings_service_spec.js | 55 +++++++ .../components/incidents_settings_tabs_spec.js | 4 +- .../components/pagerduty_form_spec.js | 67 ++++++++ spec/frontend/pipelines/graph/job_item_spec.js | 13 +- .../pipelines/graph/linked_pipeline_spec.js | 28 +++- .../pipelines/graph/linked_pipelines_mock_data.js | 21 +++ .../pipelines/test_reports/test_summary_spec.js | 2 +- .../reports/components/summary_row_spec.js | 43 +++-- .../backfill_namespace_settings_spec.rb | 23 +++ .../backfill_snippet_repositories_spec.rb | 10 +- spec/lib/gitlab/danger/helper_spec.rb | 5 + ...00703125016_backfill_namespace_settings_spec.rb | 30 ++++ spec/models/concerns/approvable_base_spec.rb | 4 +- spec/models/member_spec.rb | 22 +++ spec/models/members/group_member_spec.rb | 56 ++----- spec/models/namespace_setting_spec.rb | 7 + spec/models/namespace_spec.rb | 1 + spec/models/user_spec.rb | 6 + spec/requests/api/group_import_spec.rb | 1 + spec/requests/api/members_spec.rb | 74 ++++++--- spec/requests/api/project_milestones_spec.rb | 67 +++++++- spec/services/groups/create_service_spec.rb | 9 + 105 files changed, 1361 insertions(+), 317 deletions(-) create mode 100644 app/assets/javascripts/incidents_settings/components/pagerduty_form.vue create mode 100644 app/assets/javascripts/incidents_settings/incidents_settings_service.js create mode 100644 app/models/namespace_setting.rb create mode 100644 changelogs/unreleased/196066-add-milestone-expired-info-be-2.yml create mode 100644 changelogs/unreleased/217392-update-workhorse-version.yml create mode 100644 changelogs/unreleased/224039-jira-issues-integration-doc.yml create mode 100644 changelogs/unreleased/add-namespace-settings-table.yml create mode 100644 changelogs/unreleased/dag-annotations-sticky.yml create mode 100644 changelogs/unreleased/downstream-pipeline-ux.yml create mode 100644 changelogs/unreleased/fix-design-note-border-radius.yml create mode 100644 changelogs/unreleased/remove-ff-in-count-users-by-group.yml create mode 100644 changelogs/unreleased/xanf-expose-bitbucket-error.yml create mode 100644 db/migrate/20200703124823_create_namespace_settings.rb create mode 100644 db/post_migrate/20200703125016_backfill_namespace_settings.rb delete mode 100644 doc/user/application_security/container_scanning/img/container_scanning_v13_1.png create mode 100644 doc/user/application_security/container_scanning/img/container_scanning_v13_2.png delete mode 100644 doc/user/application_security/dast/img/dast_all_v13_1.png create mode 100644 doc/user/application_security/dast/img/dast_v13_2.png delete mode 100644 doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_1.png create mode 100644 doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_2.png delete mode 100644 doc/user/application_security/sast/img/sast_v13_1.png create mode 100644 doc/user/application_security/sast/img/sast_v13_2.png delete mode 100644 doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.png create mode 100644 doc/user/application_security/secret_detection/img/secret_detection_v13_2.png delete mode 100644 doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v12_10.png create mode 100644 doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_2.png create mode 100644 doc/user/project/integrations/img/jira/open_jira_issues_list_v13.2.png create mode 100644 lib/gitlab/background_migration/backfill_namespace_settings.rb create mode 100644 spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap create mode 100644 spec/frontend/incidents_settings/components/incidents_settings_service_spec.js create mode 100644 spec/frontend/incidents_settings/components/pagerduty_form_spec.js create mode 100644 spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb create mode 100644 spec/migrations/20200703125016_backfill_namespace_settings_spec.rb create mode 100644 spec/models/namespace_setting_spec.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 249bb35ec56..a5b80c7ca55 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,8 @@ default: - gitlab-org # All jobs are interruptible by default interruptible: true + # Default job timeout set to 90m https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10520 + timeout: 90m workflow: rules: diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index 0db4ae425e1..228747ae8d3 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -172,6 +172,7 @@ dependency_scanning: # # - 'export DAST_AUTH_URL="${DAST_WEBSITE}/users/sign_in"' # # - 'export DAST_PASSWORD="${REVIEW_APPS_ROOT_PASSWORD}"' # - /analyze -t $DAST_WEBSITE +# timeout: 4h # artifacts: # paths: # - gl-dast-report.json # GitLab-specific diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index b878bec3751..26c7a2194cc 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -9,6 +9,7 @@ cache gems: stage: test needs: ["setup-test-env"] variables: + BUNDLE_INSTALL_FLAGS: --with=production --with=development --with=test --jobs=2 --path=vendor --retry=3 --quiet SETUP_DB: "false" script: - bundle package --all --all-platforms diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index be23c04bf4c..2554e8ae98f 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.36.0 +8.37.0 diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js index 2422a1ed2e4..8d8d33f5972 100644 --- a/app/assets/javascripts/import_projects/store/actions.js +++ b/app/assets/javascripts/import_projects/store/actions.js @@ -70,8 +70,19 @@ export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo repoId: repo.id, }), ) - .catch(() => { - createFlash(s__('ImportProjects|Importing the project failed')); + .catch(e => { + const serverErrorMessage = e?.response?.data?.errors; + const flashMessage = serverErrorMessage + ? sprintf( + s__('ImportProjects|Importing the project failed: %{reason}'), + { + reason: serverErrorMessage, + }, + false, + ) + : s__('ImportProjects|Importing the project failed'); + + createFlash(flashMessage); commit(types.RECEIVE_IMPORT_ERROR, repo.id); }); diff --git a/app/assets/javascripts/incidents_settings/components/alerts_form.vue b/app/assets/javascripts/incidents_settings/components/alerts_form.vue index 40eaef55c48..a394f404ee1 100644 --- a/app/assets/javascripts/incidents_settings/components/alerts_form.vue +++ b/app/assets/javascripts/incidents_settings/components/alerts_form.vue @@ -9,15 +9,11 @@ import { GlNewDropdown, GlNewDropdownItem, } from '@gitlab/ui'; -import axios from '~/lib/utils/axios_utils'; -import { refreshCurrentPage } from '~/lib/utils/url_utility'; -import createFlash from '~/flash'; import { I18N_ALERT_SETTINGS_FORM, NO_ISSUE_TEMPLATE_SELECTED, TAKING_INCIDENT_ACTION_DOCS_LINK, ISSUE_TEMPLATES_DOCS_LINK, - ERROR_MSG, } from '../constants'; export default { @@ -31,7 +27,7 @@ export default { GlNewDropdown, GlNewDropdownItem, }, - inject: ['alertSettings', 'operationsSettingsEndpoint'], + inject: ['service', 'alertSettings'], data() { return { templates: [NO_ISSUE_TEMPLATE_SELECTED, ...this.alertSettings.templates], @@ -65,23 +61,10 @@ export default { }, updateAlertsIntegrationSettings() { this.loading = true; - return axios - .patch(this.operationsSettingsEndpoint, { - project: { - incident_management_setting_attributes: this.formData, - }, - }) - .then(() => { - refreshCurrentPage(); - }) - .catch(({ response }) => { - const message = response?.data?.message || ''; - createFlash(`${ERROR_MSG} ${message}`, 'alert'); - }) - .finally(() => { - this.loading = false; - }); + this.service.updateSettings(this.formData).catch(() => { + this.loading = false; + }); }, }, }; diff --git a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue index 763568fd2c9..0623c275c5a 100644 --- a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue +++ b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue @@ -1,6 +1,8 @@ @@ -37,7 +49,7 @@ export default { diff --git a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue new file mode 100644 index 00000000000..027848db6e9 --- /dev/null +++ b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue @@ -0,0 +1,183 @@ + + + diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js index bd6ee55ae42..b443c237f0f 100644 --- a/app/assets/javascripts/incidents_settings/constants.js +++ b/app/assets/javascripts/incidents_settings/constants.js @@ -1,5 +1,6 @@ import { __, s__ } from '~/locale'; +/* Integration tabs constants */ export const INTEGRATION_TABS_CONFIG = [ { title: s__('IncidentSettings|Alert integration'), @@ -8,8 +9,9 @@ export const INTEGRATION_TABS_CONFIG = [ }, { title: s__('IncidentSettings|PagerDuty integration'), - component: '', - active: false, + component: 'PagerDutySettingsForm', + active: true, + featureFlag: 'pagerdutyWebhook', }, { title: s__('IncidentSettings|Grafana integration'), @@ -21,12 +23,13 @@ export const INTEGRATION_TABS_CONFIG = [ export const I18N_INTEGRATION_TABS = { headerText: s__('IncidentSettings|Incidents'), expandBtnLabel: __('Expand'), - saveBtnLabel: __('Save changes'), subHeaderText: s__( 'IncidentSettings|Set up integrations with external tools to help better manage incidents.', ), }; +/* Alerts integration settings constants */ + export const I18N_ALERT_SETTINGS_FORM = { saveBtnLabel: __('Save changes'), introText: __('Action to take when receiving an alert. %{docsLink}'), @@ -48,4 +51,33 @@ export const TAKING_INCIDENT_ACTION_DOCS_LINK = export const ISSUE_TEMPLATES_DOCS_LINK = '/help/user/project/description_templates#creating-issue-templates'; +/* PagerDuty integration settings constants */ + +export const I18N_PAGERDUTY_SETTINGS_FORM = { + introText: s__( + 'PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.', + ), + activeToggle: { + label: s__('PagerDutySettings|Active'), + }, + webhookUrl: { + label: s__('PagerDutySettings|Webhook URL'), + helpText: s__( + 'PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}', + ), + helpDocsLink: s__('PagerDutySettings|configuring a webhook in PagerDuty'), + resetWebhookUrl: s__('PagerDutySettings|Reset webhook URL'), + copyToClipboard: __('Copy'), + updateErrMsg: s__('PagerDutySettings|Failed to update Webhook URL'), + updateSuccessMsg: s__('PagerDutySettings|Webhook URL update was successful'), + restKeyInfo: s__( + "PagerDutySettings|Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty.", + ), + }, + saveBtnLabel: __('Save changes'), +}; + +export const CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK = 'https://support.pagerduty.com/docs/webhooks'; + +/* common constants */ export const ERROR_MSG = __('There was an error saving your changes.'); diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js new file mode 100644 index 00000000000..bd4f5bb8820 --- /dev/null +++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js @@ -0,0 +1,32 @@ +import axios from '~/lib/utils/axios_utils'; +import { refreshCurrentPage } from '~/lib/utils/url_utility'; +import createFlash from '~/flash'; +import { ERROR_MSG } from './constants'; + +export default class IncidentsSettingsService { + constructor(settingsEndpoint, webhookUpdateEndpoint) { + this.settingsEndpoint = settingsEndpoint; + this.webhookUpdateEndpoint = webhookUpdateEndpoint; + } + + updateSettings(data) { + return axios + .patch(this.settingsEndpoint, { + project: { + incident_management_setting_attributes: data, + }, + }) + .then(() => { + refreshCurrentPage(); + }) + .catch(({ response }) => { + const message = response?.data?.message || ''; + + createFlash(`${ERROR_MSG} ${message}`, 'alert'); + }); + } + + resetWebhookUrl() { + return axios.post(this.webhookUpdateEndpoint); + } +} diff --git a/app/assets/javascripts/incidents_settings/index.js b/app/assets/javascripts/incidents_settings/index.js index 25fed0d10de..80e7d07feca 100644 --- a/app/assets/javascripts/incidents_settings/index.js +++ b/app/assets/javascripts/incidents_settings/index.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import SettingsTabs from './components/incidents_settings_tabs.vue'; +import IncidentsSettingsService from './incidents_settings_service'; export default () => { const el = document.querySelector('.js-incidents-settings'); @@ -10,19 +11,33 @@ export default () => { } const { - dataset: { operationsSettingsEndpoint, templates, createIssue, issueTemplateKey, sendEmail }, + dataset: { + operationsSettingsEndpoint, + templates, + createIssue, + issueTemplateKey, + sendEmail, + pagerdutyActive, + pagerdutyWebhookUrl, + pagerdutyResetKeyPath, + }, } = el; + const service = new IncidentsSettingsService(operationsSettingsEndpoint, pagerdutyResetKeyPath); return new Vue({ el, provide: { - operationsSettingsEndpoint, + service, alertSettings: { templates: JSON.parse(templates), createIssue: parseBoolean(createIssue), issueTemplateKey, sendEmail: parseBoolean(sendEmail), }, + pagerDutySettings: { + active: parseBoolean(pagerdutyActive), + webhookUrl: pagerdutyWebhookUrl, + }, }, render(createElement) { return createElement(SettingsTabs); diff --git a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue b/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue index dacaec6d36a..a1500166cdc 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue @@ -29,9 +29,9 @@ export default { return [ 'gl-display-flex', 'gl-flex-direction-column', - 'gl-absolute', + 'gl-fixed', 'gl-right-1', - 'gl-top-0', + 'gl-top-66vh', 'gl-w-max-content', 'gl-px-5', 'gl-py-4', diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 02fbf342ba7..6b890688a48 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -43,6 +43,7 @@ export default { data() { return { downstreamMarginTop: null, + jobName: null, }; }, computed: { @@ -91,13 +92,9 @@ export default { /** * Calculates the margin top of the clicked downstream pipeline by * subtracting the clicked downstream pipelines offsetTop by it's parent's - * offsetTop and then subtracting either 15 (if child) or 30 (if not a child) - * due to the height of node and stage name margin bottom. + * offsetTop and then subtracting 15 */ - this.downstreamMarginTop = this.calculateMarginTop( - downstreamNode, - downstreamNode.classList.contains('child-pipeline') ? 15 : 30, - ); + this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15); /** * If the expanded trigger is defined and the id is different than the @@ -120,6 +117,9 @@ export default { hasUpstream(index) { return index === 0 && this.hasTriggeredBy; }, + setJob(jobName) { + this.jobName = jobName; + }, }, }; @@ -180,6 +180,7 @@ export default { :is-first-column="isFirstColumn(index)" :has-triggered-by="hasTriggeredBy" :action="stage.status.action" + :job-hovered="jobName" @refreshPipelineGraph="refreshPipelineGraph" /> @@ -191,6 +192,7 @@ export default { :project-id="pipelineProjectId" graph-position="right" @linkedPipelineClick="handleClickedDownstream" + @downstreamHovered="setJob" /> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index 5aad49b05d1..733553e02c0 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -1,7 +1,7 @@ @@ -76,8 +90,10 @@ export default {
  • - {{ projectName }} • #{{ pipeline.id }} -
    - {{ label }} + {{ downstreamTitle }} • #{{ pipeline.id }} +
    + {{ label }}
  • diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 8d99ce6704e..c4dfd3382a2 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -41,6 +41,9 @@ export default { onPipelineClick(downstreamNode, pipeline, index) { this.$emit('linkedPipelineClick', pipeline, index, downstreamNode); }, + onDownstreamHovered(jobName) { + this.$emit('downstreamHovered', jobName); + }, }, }; @@ -61,6 +64,7 @@ export default { :column-title="columnTitle" :project-id="projectId" @pipelineClicked="onPipelineClick($event, pipeline, index)" + @downstreamHovered="onDownstreamHovered" /> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index bed0ed51d5f..9de6ba819c2 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -36,6 +36,11 @@ export default { required: false, default: () => ({}), }, + jobHovered: { + type: String, + required: false, + default: '', + }, }, computed: { hasAction() { @@ -80,6 +85,7 @@ export default { diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue index dee82eb5c42..d57b1466177 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue @@ -43,7 +43,7 @@ export default {
    - {{ __('Class') }} + {{ __('Suite') }}
    {{ __('Name') }} @@ -70,7 +70,7 @@ export default { class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row" >
    -
    {{ __('Class') }}
    +
    {{ __('Suite') }}
    {{ - sprintf(s__('TestReports|%{count} jobs'), { count: report.total_count }) + sprintf(s__('TestReports|%{count} tests'), { count: report.total_count }) }}
    diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue index e6a5fa4fa3e..6cfb795595d 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue @@ -17,7 +17,7 @@ export default { heading: { type: String, required: false, - default: s__('TestReports|Test suites'), + default: s__('TestReports|Jobs'), }, }, computed: { @@ -47,7 +47,7 @@ export default {
    - {{ __('Suite') }} + {{ __('Job') }}
    {{ __('Duration') }} diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue index 65efc24e968..3232c0edf96 100644 --- a/app/assets/javascripts/reports/components/summary_row.vue +++ b/app/assets/javascripts/reports/components/summary_row.vue @@ -21,7 +21,8 @@ export default { props: { summary: { type: String, - required: true, + required: false, + default: '', }, statusIcon: { type: String, @@ -58,8 +59,8 @@ export default { class="report-block-list-issue-description-text" data-testid="test-summary-row-description" > - {{ summary - }}{{ summary }} 
    diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss index 8f632b87376..33f03fb5949 100644 --- a/app/assets/stylesheets/components/design_management/design.scss +++ b/app/assets/stylesheets/components/design_management/design.scss @@ -109,6 +109,8 @@ padding: $gl-padding; list-style: none; transition: background $gl-transition-duration-medium $general-hover-transition-curve; + border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box + border-top-right-radius: $border-radius-default; a { color: inherit; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index ef50bcfc2f9..57ad9abef4b 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1101,7 +1101,3 @@ button.mini-pipeline-graph-dropdown-toggle { .progress-bar.bg-primary { background-color: $blue-500 !important; } - -.parent-child-label-container { - padding-top: $gl-padding-4; -} diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 7164af213db..9085af0ff1c 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -109,3 +109,7 @@ .gl-transition-property-stroke { transition-property: stroke; } + +.gl-top-66vh { + top: 66vh; +} diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb index 02e306bc500..6323bd01c58 100644 --- a/app/models/concerns/approvable_base.rb +++ b/app/models/concerns/approvable_base.rb @@ -8,7 +8,7 @@ module ApprovableBase has_many :approved_by_users, through: :approvals, source: :user end - def has_approved?(user) + def approved_by?(user) return false unless user approved_by_users.include?(user) diff --git a/app/models/member.rb b/app/models/member.rb index f2926d32d47..36f9741ce01 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -38,6 +38,11 @@ class Member < ApplicationRecord scope: [:source_type, :source_id], allow_nil: true } + validates :user_id, + uniqueness: { + message: _('project bots cannot be added to other groups / projects') + }, + if: :project_bot? # This scope encapsulates (most of) the conditions a row in the member table # must satisfy if it is a valid permission. Of particular note: @@ -473,6 +478,10 @@ class Member < ApplicationRecord def update_highest_role_attribute user_id end + + def project_bot? + user&.project_bot? + end end Member.prepend_if_ee('EE::Member') diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 9a916cd40ae..8c224dea88f 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -17,14 +17,7 @@ class GroupMember < Member scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } scope :of_ldap_type, -> { where(ldap: true) } - - scope :count_users_by_group_id, -> do - if Feature.enabled?(:optimized_count_users_by_group_id) - group(:source_id).count - else - joins(:user).group(:source_id).count - end - end + scope :count_users_by_group_id, -> { group(:source_id).count } after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/models/namespace.rb b/app/models/namespace.rb index feac98fc72f..e529ba6b486 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -22,6 +22,7 @@ class Namespace < ApplicationRecord has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_statistics + has_one :namespace_settings, inverse_of: :namespace, class_name: 'NamespaceSetting', autosave: true has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace' has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner' diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb new file mode 100644 index 00000000000..53bfa3d979e --- /dev/null +++ b/app/models/namespace_setting.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class NamespaceSetting < ApplicationRecord + belongs_to :namespace, inverse_of: :namespace_settings + + self.primary_key = :namespace_id +end + +NamespaceSetting.prepend_if_ee('EE::NamespaceSetting') diff --git a/app/models/user.rb b/app/models/user.rb index 36b9ed358ff..643b759e6f4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1270,7 +1270,8 @@ class User < ApplicationRecord namespace.path = username if username_changed? namespace.name = name if name_changed? else - build_namespace(path: username, name: name) + namespace = build_namespace(path: username, name: name) + namespace.build_namespace_settings end end diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb index eb1b8d4fcc0..ce583095168 100644 --- a/app/services/groups/create_service.rb +++ b/app/services/groups/create_service.rb @@ -28,7 +28,11 @@ module Groups @group.build_chat_team(name: response['name'], team_id: response['id']) end - @group.add_owner(current_user) if @group.save + if @group.save + @group.add_owner(current_user) + add_settings_record + end + @group end @@ -79,6 +83,10 @@ module Groups params[:visibility_level] = Gitlab::CurrentSettings.current_application_settings.default_group_visibility end + + def add_settings_record + @group.create_namespace_settings + end end end diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb index 0b729981a93..610288c5e76 100644 --- a/app/services/members/create_service.rb +++ b/app/services/members/create_service.rb @@ -22,7 +22,7 @@ module Members errors = [] members.each do |member| - if member.errors.any? + if member.invalid? current_error = # Invited users may not have an associated user if member.user.present? diff --git a/app/services/merge_requests/remove_approval_service.rb b/app/services/merge_requests/remove_approval_service.rb index 5bc44bdad00..3164d0b4069 100644 --- a/app/services/merge_requests/remove_approval_service.rb +++ b/app/services/merge_requests/remove_approval_service.rb @@ -4,7 +4,7 @@ module MergeRequests class RemoveApprovalService < MergeRequests::BaseService # rubocop: disable CodeReuse/ActiveRecord def execute(merge_request) - return unless merge_request.has_approved?(current_user) + return unless merge_request.approved_by?(current_user) # paranoid protection against running wrong deletes return unless merge_request.id && current_user.id diff --git a/changelogs/unreleased/196066-add-milestone-expired-info-be-2.yml b/changelogs/unreleased/196066-add-milestone-expired-info-be-2.yml new file mode 100644 index 00000000000..6455216d16a --- /dev/null +++ b/changelogs/unreleased/196066-add-milestone-expired-info-be-2.yml @@ -0,0 +1,5 @@ +--- +title: Add include_parent_milestones param to milestones API +merge_request: 36944 +author: +type: added diff --git a/changelogs/unreleased/217392-update-workhorse-version.yml b/changelogs/unreleased/217392-update-workhorse-version.yml new file mode 100644 index 00000000000..7e7cf835b7a --- /dev/null +++ b/changelogs/unreleased/217392-update-workhorse-version.yml @@ -0,0 +1,5 @@ +--- +title: Update GITLAB_WORKHORSE_VERSION to 8.37.0 +merge_request: 36988 +author: +type: other diff --git a/changelogs/unreleased/224039-jira-issues-integration-doc.yml b/changelogs/unreleased/224039-jira-issues-integration-doc.yml new file mode 100644 index 00000000000..0262543c574 --- /dev/null +++ b/changelogs/unreleased/224039-jira-issues-integration-doc.yml @@ -0,0 +1,6 @@ +--- +title: Expands Jira integration to allow viewing and searching a list of of Jira issues + directly within GitLab +merge_request: 36435 +author: +type: added diff --git a/changelogs/unreleased/add-namespace-settings-table.yml b/changelogs/unreleased/add-namespace-settings-table.yml new file mode 100644 index 00000000000..7e09b391efb --- /dev/null +++ b/changelogs/unreleased/add-namespace-settings-table.yml @@ -0,0 +1,5 @@ +--- +title: Add namespace settings table +merge_request: 36321 +author: +type: added diff --git a/changelogs/unreleased/dag-annotations-sticky.yml b/changelogs/unreleased/dag-annotations-sticky.yml new file mode 100644 index 00000000000..763ee558e97 --- /dev/null +++ b/changelogs/unreleased/dag-annotations-sticky.yml @@ -0,0 +1,5 @@ +--- +title: Make DAG annotations stick +merge_request: 37068 +author: +type: changed diff --git a/changelogs/unreleased/downstream-pipeline-ux.yml b/changelogs/unreleased/downstream-pipeline-ux.yml new file mode 100644 index 00000000000..d8aec39bc41 --- /dev/null +++ b/changelogs/unreleased/downstream-pipeline-ux.yml @@ -0,0 +1,5 @@ +--- +title: Add correlation between trigger job and child pipeline +merge_request: 36750 +author: +type: changed diff --git a/changelogs/unreleased/fix-design-note-border-radius.yml b/changelogs/unreleased/fix-design-note-border-radius.yml new file mode 100644 index 00000000000..af67e4ffce8 --- /dev/null +++ b/changelogs/unreleased/fix-design-note-border-radius.yml @@ -0,0 +1,5 @@ +--- +title: Fix background overflow when design note is selected +merge_request: 36931 +author: +type: fixed diff --git a/changelogs/unreleased/remove-ff-in-count-users-by-group.yml b/changelogs/unreleased/remove-ff-in-count-users-by-group.yml new file mode 100644 index 00000000000..0a1973fcf25 --- /dev/null +++ b/changelogs/unreleased/remove-ff-in-count-users-by-group.yml @@ -0,0 +1,5 @@ +--- +title: Remove optimized_count_users_by_group_id feature flag +merge_request: 36953 +author: +type: performance diff --git a/changelogs/unreleased/xanf-expose-bitbucket-error.yml b/changelogs/unreleased/xanf-expose-bitbucket-error.yml new file mode 100644 index 00000000000..571bc65bc30 --- /dev/null +++ b/changelogs/unreleased/xanf-expose-bitbucket-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix displaying import errors from server +merge_request: 37073 +author: +type: fixed diff --git a/db/migrate/20200703124823_create_namespace_settings.rb b/db/migrate/20200703124823_create_namespace_settings.rb new file mode 100644 index 00000000000..907b9d2ca8c --- /dev/null +++ b/db/migrate/20200703124823_create_namespace_settings.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class CreateNamespaceSettings < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + with_lock_retries do + create_table :namespace_settings, id: false do |t| + t.timestamps_with_timezone null: false + t.references :namespace, primary_key: true, default: nil, type: :integer, index: false, foreign_key: { on_delete: :cascade } + end + end + end + + def down + drop_table :namespace_settings + end +end diff --git a/db/post_migrate/20200703125016_backfill_namespace_settings.rb b/db/post_migrate/20200703125016_backfill_namespace_settings.rb new file mode 100644 index 00000000000..a7335e2d2b8 --- /dev/null +++ b/db/post_migrate/20200703125016_backfill_namespace_settings.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class BackfillNamespaceSettings < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + MIGRATION = 'BackfillNamespaceSettings' + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 10_000 + + disable_ddl_transaction! + + class Namespace < ActiveRecord::Base + include EachBatch + + self.table_name = 'namespaces' + end + + def up + say "Scheduling `#{MIGRATION}` jobs" + + queue_background_migration_jobs_by_range_at_intervals(Namespace, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + # NOOP + end +end diff --git a/db/structure.sql b/db/structure.sql index c994885f5d1..d79e2f4eeba 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13118,6 +13118,12 @@ CREATE TABLE public.namespace_root_storage_statistics ( snippets_size bigint DEFAULT 0 NOT NULL ); +CREATE TABLE public.namespace_settings ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + namespace_id integer NOT NULL +); + CREATE TABLE public.namespace_statistics ( id integer NOT NULL, namespace_id integer NOT NULL, @@ -17894,6 +17900,9 @@ ALTER TABLE ONLY public.namespace_limits ALTER TABLE ONLY public.namespace_root_storage_statistics ADD CONSTRAINT namespace_root_storage_statistics_pkey PRIMARY KEY (namespace_id); +ALTER TABLE ONLY public.namespace_settings + ADD CONSTRAINT namespace_settings_pkey PRIMARY KEY (namespace_id); + ALTER TABLE ONLY public.namespace_statistics ADD CONSTRAINT namespace_statistics_pkey PRIMARY KEY (id); @@ -21776,6 +21785,9 @@ ALTER TABLE ONLY public.analytics_cycle_analytics_project_stages ALTER TABLE ONLY public.issue_user_mentions ADD CONSTRAINT fk_rails_3861d9fefa FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.namespace_settings + ADD CONSTRAINT fk_rails_3896d4fae5 FOREIGN KEY (namespace_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.self_managed_prometheus_alert_events ADD CONSTRAINT fk_rails_3936dadc62 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; @@ -23825,6 +23837,8 @@ COPY "schema_migrations" (version) FROM STDIN; 20200702201039 20200703064117 20200703121557 +20200703124823 +20200703125016 20200703154822 20200704143633 20200704161600 diff --git a/doc/api/milestones.md b/doc/api/milestones.md index b5702c7d6e0..b3a6e372b4c 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -25,13 +25,14 @@ GET /projects/:id/milestones?search=version Parameters: -| 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 | -| `iids[]` | integer array | optional | Return only the milestones having the given `iid` | -| `state` | string | optional | Return only `active` or `closed` milestones | -| `title` | string | optional | Return only the milestones having the given `title` | -| `search` | string | optional | Return only milestones with a title or description matching the provided string | +| 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 | +| `iids[]` | integer array | optional | Return only the milestones having the given `iid`. Will be ignored if `include_parent_milestones` is set to `true` | +| `state` | string | optional | Return only `active` or `closed` milestones | +| `title` | string | optional | Return only the milestones having the given `title` | +| `search` | string | optional | Return only milestones with a title or description matching the provided string | +| `include_parent_milestones` | boolean | optional | Include milestones from parent group and ancestors. Introduced in [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36944) | ```shell curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/milestones" diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 9910f0651b8..54f8ca0d98b 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -9,7 +9,7 @@ pipeline](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6665). ```mermaid graph TD - A["build-qa-image, gitlab:assets:compile pull-cache
    (canonical default refs only)"]; + A["build-qa-image, compile-production-assets
    (canonical default refs only)"]; B[review-build-cng]; C[review-deploy]; D[CNG-mirror]; @@ -44,23 +44,25 @@ subgraph "CNG-mirror pipeline" ### Detailed explanation -1. On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) during the `test` stage, the - [`gitlab:assets:compile`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724487) job is automatically started. - - Once it's done, it starts the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808) - manual job since the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) pipeline triggered in the +1. On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) during the `prepare` stage, the + [`compile-production-assets`](https://gitlab.com/gitlab-org/gitlab/-/jobs/641770154) job is automatically started. + - Once it's done, the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808) + job starts since the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) pipeline triggered in the following step depends on it. -1. The [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808) job [triggers a pipeline](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657) +1. Once `compile-production-assets` is done, the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808) + job [triggers a pipeline](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657) in the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) project. + - The `review-build-cng` job automatically starts only if your MR includes + [CI or frontend changes](../pipelines.md#changes-patterns). In other cases, the job is manual. - The [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657) pipeline creates the Docker images of each component (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.) based on the commit from the [GitLab pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) and stores them in its [registry](https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry). - We use the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) project so that the `CNG`, (Cloud - Native GitLab), project's registry is not overloaded with a - lot of transient Docker images. + Native GitLab), project's registry is not overloaded with a lot of transient Docker images. - Note that the official CNG images are built by the `cloud-native-image` job, which runs only for tags, and triggers itself a [`CNG`](https://gitlab.com/gitlab-org/build/CNG) pipeline. -1. Once the `test` stage is done, the [`review-deploy`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724810) job +1. Once `review-build-cng` is done, the [`review-deploy`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724810) job deploys the Review App using [the official GitLab Helm chart](https://gitlab.com/gitlab-org/charts/gitlab/) to the [`review-apps`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps?project=gitlab-review-apps) Kubernetes cluster on GCP. @@ -94,10 +96,9 @@ subgraph "CNG-mirror pipeline" - The manual `review-stop` can be used to stop a Review App manually, and is also started by GitLab once a merge request's branch is deleted after being merged. -- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using +- The Kubernetes cluster is connected to the `gitlab` projects using [GitLab's Kubernetes integration](../../user/project/clusters/index.md). This basically - allows to have a link to the Review App directly from the merge request - widget. + allows to have a link to the Review App directly from the merge request widget. ### Auto-stopping of Review Apps diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md index 0f58b18734a..61e730ce09b 100644 --- a/doc/user/application_security/configuration/index.md +++ b/doc/user/application_security/configuration/index.md @@ -26,6 +26,11 @@ all security features will be configured by default. ## Limitations -It is not possible to enable or disable a feature using the configuration page. -However, instructions on how to enable or disable a feature can be found through -the links next to each feature on that page. +It is not yet possible to enable or disable most features using the +configuration page. However, instructions on how to enable or disable a feature +can be found through the links next to each feature on that page. + +If a project does not have an existing CI configuration, then the SAST feature +can be enabled by clicking on the "Enable with Merge Request" button under the +"Manage" column. Future work will expand this to editing _existing_ CI +configurations, and to other security features. diff --git a/doc/user/application_security/container_scanning/img/container_scanning_v13_1.png b/doc/user/application_security/container_scanning/img/container_scanning_v13_1.png deleted file mode 100644 index 966296798ad..00000000000 Binary files a/doc/user/application_security/container_scanning/img/container_scanning_v13_1.png and /dev/null differ diff --git a/doc/user/application_security/container_scanning/img/container_scanning_v13_2.png b/doc/user/application_security/container_scanning/img/container_scanning_v13_2.png new file mode 100644 index 00000000000..254ea1dcf5d Binary files /dev/null and b/doc/user/application_security/container_scanning/img/container_scanning_v13_2.png differ diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index f6b0d661ba7..7bc8b62825c 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -32,7 +32,7 @@ You can enable container scanning by doing one of the following: GitLab compares the found vulnerabilities between the source and target branches, and shows the information directly in the merge request. -![Container Scanning Widget](img/container_scanning_v13_1.png) +![Container Scanning Widget](img/container_scanning_v13_2.png) + + +
    diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap new file mode 100644 index 00000000000..17ada722034 --- /dev/null +++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Alert integration settings form should match the default snapshot 1`] = ` +
    + + +

    + Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident. +

    + +
    + + + + + + + +
    + +
    + + + + Reset webhook URL + + + + + + Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty. + + +
    + + + + Save changes + + +
    +
    +`; diff --git a/spec/frontend/incidents_settings/components/alerts_form_spec.js b/spec/frontend/incidents_settings/components/alerts_form_spec.js index 2a27347e40e..04832f31e58 100644 --- a/spec/frontend/incidents_settings/components/alerts_form_spec.js +++ b/spec/frontend/incidents_settings/components/alerts_form_spec.js @@ -1,24 +1,16 @@ import { shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; import AlertsSettingsForm from '~/incidents_settings/components/alerts_form.vue'; -import { ERROR_MSG } from '~/incidents_settings/constants'; -import createFlash from '~/flash'; -import { refreshCurrentPage } from '~/lib/utils/url_utility'; -import waitForPromises from 'helpers/wait_for_promises'; - -jest.mock('~/flash'); -jest.mock('~/lib/utils/url_utility'); describe('Alert integration settings form', () => { let wrapper; + const service = { updateSettings: jest.fn().mockResolvedValue() }; const findForm = () => wrapper.find({ ref: 'settingsForm' }); beforeEach(() => { wrapper = shallowMount(AlertsSettingsForm, { provide: { - operationsSettingsEndpoint: 'operations/endpoint', + service, alertSettings: { issueTemplateKey: 'selecte_tmpl', createIssue: true, @@ -32,6 +24,7 @@ describe('Alert integration settings form', () => { afterEach(() => { if (wrapper) { wrapper.destroy(); + wrapper = null; } }); @@ -42,30 +35,15 @@ describe('Alert integration settings form', () => { }); describe('form', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - }); - - it('should refresh the page on successful submit', () => { - mock.onPatch().reply(200); - findForm().trigger('submit'); - return waitForPromises().then(() => { - expect(refreshCurrentPage).toHaveBeenCalled(); - }); - }); - - it('should display a flah message on unsuccessful submit', () => { - mock.onPatch().reply(400); + it('should call service `updateSettings` on submit', () => { findForm().trigger('submit'); - return waitForPromises().then(() => { - expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert'); - }); + expect(service.updateSettings).toHaveBeenCalledWith( + expect.objectContaining({ + create_issue: wrapper.vm.createIssueEnabled, + issue_template_key: wrapper.vm.issueTemplate, + send_email: wrapper.vm.sendEmailEnabled, + }), + ); }); }); }); diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js new file mode 100644 index 00000000000..58f9a318808 --- /dev/null +++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js @@ -0,0 +1,55 @@ +import axios from '~/lib/utils/axios_utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import httpStatusCodes from '~/lib/utils/http_status'; +import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service'; +import { ERROR_MSG } from '~/incidents_settings/constants'; +import createFlash from '~/flash'; +import { refreshCurrentPage } from '~/lib/utils/url_utility'; + +jest.mock('~/flash'); +jest.mock('~/lib/utils/url_utility'); + +describe('IncidentsSettingsService', () => { + const settingsEndpoint = 'operations/settings'; + const webhookUpdateEndpoint = 'webhook/update'; + let mock; + let service; + + beforeEach(() => { + mock = new AxiosMockAdapter(axios); + service = new IncidentsSettingsService(settingsEndpoint, webhookUpdateEndpoint); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('updateSettings', () => { + it('should refresh the page on successful update', () => { + mock.onPatch().reply(httpStatusCodes.OK); + + return service.updateSettings({}).then(() => { + expect(refreshCurrentPage).toHaveBeenCalled(); + }); + }); + + it('should display a flash message on update error', () => { + mock.onPatch().reply(httpStatusCodes.BAD_REQUEST); + + return service.updateSettings({}).then(() => { + expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert'); + }); + }); + }); + + describe('resetWebhookUrl', () => { + it('should make a call for webhook update', () => { + jest.spyOn(axios, 'post'); + mock.onPost().reply(httpStatusCodes.OK); + + return service.resetWebhookUrl().then(() => { + expect(axios.post).toHaveBeenCalledWith(webhookUpdateEndpoint); + }); + }); + }); +}); diff --git a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js index c56b9ed2a69..47e2aecc108 100644 --- a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js +++ b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js @@ -6,7 +6,9 @@ describe('IncidentsSettingTabs', () => { let wrapper; beforeEach(() => { - wrapper = shallowMount(IncidentsSettingTabs); + wrapper = shallowMount(IncidentsSettingTabs, { + provide: { glFeatures: { pagerdutyWebhook: true } }, + }); }); afterEach(() => { diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js new file mode 100644 index 00000000000..521094ad54c --- /dev/null +++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js @@ -0,0 +1,67 @@ +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import PagerDutySettingsForm from '~/incidents_settings/components/pagerduty_form.vue'; +import { GlAlert, GlModal } from '@gitlab/ui'; + +describe('Alert integration settings form', () => { + let wrapper; + const resetWebhookUrl = jest.fn(); + const service = { updateSettings: jest.fn().mockResolvedValue(), resetWebhookUrl }; + + const findForm = () => wrapper.find({ ref: 'settingsForm' }); + const findWebhookInput = () => wrapper.find('[data-testid="webhook-url"]'); + const findModal = () => wrapper.find(GlModal); + const findAlert = () => wrapper.find(GlAlert); + + beforeEach(() => { + wrapper = shallowMount(PagerDutySettingsForm, { + provide: { + service, + pagerDutySettings: { + active: true, + webhookUrl: 'pagerduty.webhook.com', + webhookUpdateEndpoint: 'webhook/update', + }, + }, + }); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + it('should match the default snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('should call service `updateSettings` on form submit', () => { + findForm().trigger('submit'); + expect(service.updateSettings).toHaveBeenCalledWith( + expect.objectContaining({ pagerduty_active: wrapper.vm.active }), + ); + }); + + describe('Webhook reset', () => { + it('should make a call for webhook reset and reset form values', async () => { + const newWebhookUrl = 'new.webhook.url?token=token'; + resetWebhookUrl.mockResolvedValueOnce({ + data: { pagerduty_webhook_url: newWebhookUrl }, + }); + findModal().vm.$emit('ok'); + await waitForPromises(); + expect(resetWebhookUrl).toHaveBeenCalled(); + expect(findWebhookInput().attributes('value')).toBe(newWebhookUrl); + expect(findAlert().attributes('variant')).toBe('success'); + }); + + it('should show error message and NOT reset webhook url', async () => { + resetWebhookUrl.mockRejectedValueOnce(); + findModal().vm.$emit('ok'); + await waitForPromises(); + expect(findAlert().attributes('variant')).toBe('danger'); + }); + }); +}); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index da777466e3e..2c5e7a1f6e9 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -5,6 +5,8 @@ import JobItem from '~/pipelines/components/graph/job_item.vue'; describe('pipeline graph job item', () => { let wrapper; + const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]'); + const createWrapper = propsData => { wrapper = mount(JobItem, { propsData, @@ -57,7 +59,7 @@ describe('pipeline graph job item', () => { }); describe('name without link', () => { - it('it should render status and name', () => { + beforeEach(() => { createWrapper({ job: { id: 4257, @@ -71,13 +73,22 @@ describe('pipeline graph job item', () => { has_details: false, }, }, + cssClassJobName: 'css-class-job-name', + jobHovered: 'test', }); + }); + it('it should render status and name', () => { expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(wrapper.find('a').exists()).toBe(false); expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name); }); + + it('should apply hover class and provided class name', () => { + expect(findJobWithoutLink().classes()).toContain('gl-inset-border-1-blue-500'); + expect(findJobWithoutLink().classes()).toContain('css-class-job-name'); + }); }); describe('action icon', () => { diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index cf78aa3ef71..133d5695afb 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -11,7 +11,10 @@ const invalidTriggeredPipelineId = mockPipeline.project.id + 5; describe('Linked pipeline', () => { let wrapper; + const findButton = () => wrapper.find('button'); + const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]'); + const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); const createWrapper = propsData => { wrapper = mount(LinkedPipelineComponent, { @@ -69,6 +72,8 @@ describe('Linked pipeline', () => { it('should correctly compute the tooltip text', () => { expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name); expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label); + expect(wrapper.vm.tooltipText).toContain(mockPipeline.source_job.name); + expect(wrapper.vm.tooltipText).toContain(mockPipeline.id); }); it('should render the tooltip text as the title attribute', () => { @@ -83,9 +88,8 @@ describe('Linked pipeline', () => { expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(false); }); - it('should not display child label when pipeline project id is not the same as triggered pipeline project id', () => { - const labelContainer = wrapper.find('.parent-child-label-container'); - expect(labelContainer.exists()).toBe(false); + it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => { + expect(findPipelineLabel().text()).toBe('Multi-project'); }); }); @@ -103,17 +107,17 @@ describe('Linked pipeline', () => { it('parent/child label container should exist', () => { createWrapper(downstreamProps); - expect(wrapper.find('.parent-child-label-container').exists()).toBe(true); + expect(findPipelineLabel().exists()).toBe(true); }); it('should display child label when pipeline project id is the same as triggered pipeline project id', () => { createWrapper(downstreamProps); - expect(wrapper.find('.parent-child-label-container').text()).toContain('Child'); + expect(findPipelineLabel().exists()).toBe(true); }); it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => { createWrapper(upstreamProps); - expect(wrapper.find('.parent-child-label-container').text()).toContain('Parent'); + expect(findPipelineLabel().exists()).toBe(true); }); }); @@ -133,7 +137,7 @@ describe('Linked pipeline', () => { }); }); - describe('on click', () => { + describe('on click/hover', () => { const props = { pipeline: mockPipeline, projectId: validTriggeredPipelineId, @@ -160,5 +164,15 @@ describe('Linked pipeline', () => { 'js-linked-pipeline-34993051', ]); }); + + it('should emit downstreamHovered with job name on mouseover', () => { + findLinkedPipeline().trigger('mouseover'); + expect(wrapper.emitted().downstreamHovered).toStrictEqual([['trigger_job']]); + }); + + it('should emit downstreamHovered with empty string on mouseleave', () => { + findLinkedPipeline().trigger('mouseleave'); + expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]); + }); }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js index 3e9c0814403..5756a666ff3 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js @@ -14,6 +14,9 @@ export default { active: false, coverage: null, source: 'push', + source_job: { + name: 'trigger_job', + }, created_at: '2018-06-05T11:31:30.452Z', updated_at: '2018-10-31T16:35:31.305Z', path: '/gitlab-org/gitlab-runner/pipelines/23211253', @@ -381,6 +384,9 @@ export default { active: false, coverage: null, source: 'pipeline', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { @@ -889,6 +895,9 @@ export default { active: false, coverage: null, source: 'pipeline', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { @@ -1402,6 +1411,9 @@ export default { active: false, coverage: null, source: 'pipeline', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { @@ -1912,6 +1924,9 @@ export default { active: false, coverage: null, source: 'pipeline', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { @@ -2412,6 +2427,9 @@ export default { active: false, coverage: null, source: 'push', + source_job: { + name: 'trigger_job', + }, created_at: '2019-01-06T17:48:37.599Z', updated_at: '2019-01-06T17:48:38.371Z', path: '/h5bp/html5-boilerplate/pipelines/26', @@ -3743,6 +3761,9 @@ export default { active: false, coverage: null, source: 'push', + source_job: { + name: 'trigger_job', + }, path: '/gitlab-org/gitlab-test/pipelines/4', details: { status: { diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js index 8f041e46472..79be6c168cf 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js @@ -60,7 +60,7 @@ describe('Test reports summary', () => { }); it('displays the correct total', () => { - expect(totalTests().text()).toBe('4 jobs'); + expect(totalTests().text()).toBe('4 tests'); }); it('displays the correct failure count', () => { diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/reports/components/summary_row_spec.js index cb0cc025e80..85c68ed069b 100644 --- a/spec/frontend/reports/components/summary_row_spec.js +++ b/spec/frontend/reports/components/summary_row_spec.js @@ -1,10 +1,8 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import component from '~/reports/components/summary_row.vue'; +import { mount } from '@vue/test-utils'; +import SummaryRow from '~/reports/components/summary_row.vue'; describe('Summary row', () => { - const Component = Vue.extend(component); - let vm; + let wrapper; const props = { summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability', @@ -15,23 +13,42 @@ describe('Summary row', () => { statusIcon: 'warning', }; - beforeEach(() => { - vm = mountComponent(Component, props); - }); + const createComponent = ({ propsData = {}, slots = {} } = {}) => { + wrapper = mount(SummaryRow, { + propsData: { + ...props, + ...propsData, + }, + slots, + }); + }; + + const findSummary = () => wrapper.find('.report-block-list-issue-description-text'); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); it('renders provided summary', () => { - expect( - vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(), - ).toEqual(props.summary); + createComponent(); + expect(findSummary().text()).toEqual(props.summary); }); it('renders provided icon', () => { - expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain( + createComponent(); + expect(wrapper.find('.report-block-list-icon span').classes()).toContain( 'js-ci-status-icon-warning', ); }); + + describe('summary slot', () => { + it('replaces the summary prop', () => { + const summarySlotContent = 'Summary slot content'; + createComponent({ slots: { summary: summarySlotContent } }); + + expect(wrapper.text()).not.toContain(props.summary); + expect(findSummary().text()).toEqual(summarySlotContent); + }); + }); }); diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb new file mode 100644 index 00000000000..43e76a2952e --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceSettings, schema: 20200703125016 do + let(:namespaces) { table(:namespaces) } + let(:namespace_settings) { table(:namespace_settings) } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + + subject { described_class.new } + + describe '#perform' do + it 'creates settings for all projects in range' do + namespaces.create!(id: 5, name: 'test1', path: 'test1') + namespaces.create!(id: 7, name: 'test2', path: 'test2') + namespaces.create!(id: 8, name: 'test3', path: 'test3') + + subject.perform(5, 7) + + expect(namespace_settings.all.pluck(:namespace_id)).to contain_exactly(5, 7) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index 6d416c48530..ec2fd3cc4e0 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -24,7 +24,15 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat confirmed_at: 1.day.ago) end - let(:migration_bot) { User.migration_bot } + let!(:migration_bot) do + users.create(id: 100, + email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}", + user_type: HasUserType::USER_TYPES[:migration_bot], + name: 'GitLab Migration Bot', + projects_limit: 10, + username: 'bot') + end + let!(:snippet_with_repo) { snippets.create(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } let!(:snippet_with_empty_repo) { snippets.create(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } let!(:snippet_without_repo) { snippets.create(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index bc8a34cd553..e73742b5911 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -363,6 +363,11 @@ RSpec.describe Gitlab::Danger::Helper do where(:mr_title, :expected_mr_title) do 'My MR title' | 'My MR title' 'WIP: My MR title' | 'My MR title' + 'Draft: My MR title' | 'My MR title' + '(Draft) My MR title' | 'My MR title' + '[Draft] My MR title' | 'My MR title' + '[DRAFT] My MR title' | 'My MR title' + 'DRAFT: My MR title' | 'My MR title' end with_them do diff --git a/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb b/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb new file mode 100644 index 00000000000..7b84ef9e236 --- /dev/null +++ b/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200703125016_backfill_namespace_settings.rb') + +RSpec.describe BackfillNamespaceSettings, :sidekiq, schema: 20200703124823 do + let(:namespaces) { table(:namespaces) } + + describe '#up' do + before do + stub_const("#{described_class}::BATCH_SIZE", 2) + + namespaces.create!(id: 1, name: 'test1', path: 'test1') + namespaces.create!(id: 2, name: 'test2', path: 'test2') + namespaces.create!(id: 3, name: 'test3', path: 'test3') + end + + it 'schedules BackfillNamespaceSettings background jobs' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 3, 3) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + end +end diff --git a/spec/models/concerns/approvable_base_spec.rb b/spec/models/concerns/approvable_base_spec.rb index e4aded1b8d0..8fda8bccf09 100644 --- a/spec/models/concerns/approvable_base_spec.rb +++ b/spec/models/concerns/approvable_base_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' RSpec.describe ApprovableBase do - describe '#has_approved?' do + describe '#approved_by?' do let(:merge_request) { create(:merge_request) } let(:user) { create(:user) } - subject { merge_request.has_approved?(user) } + subject { merge_request.approved_by?(user) } context 'when a user has not approved' do it 'returns false' do diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 6cb35fd1be8..f155c240fb2 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -88,6 +88,28 @@ RSpec.describe Member do expect(child_member).to be_valid end end + + context 'project bots' do + let_it_be(:project_bot) { create(:user, :project_bot) } + let(:new_member) { build(:project_member, user_id: project_bot.id) } + + context 'not a member of any group or project' do + it 'is valid' do + expect(new_member).to be_valid + end + end + + context 'already member of a project' do + before do + unrelated_project = create(:project) + unrelated_project.add_maintainer(project_bot) + end + + it 'is not valid' do + expect(new_member).not_to be_valid + end + end + end end describe 'Scopes & finders' do diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 94091d5268e..9af620e70a5 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -4,50 +4,18 @@ require 'spec_helper' RSpec.describe GroupMember do context 'scopes' do - shared_examples '.count_users_by_group_id' do - it 'counts users by group ID' do - user_1 = create(:user) - user_2 = create(:user) - group_1 = create(:group) - group_2 = create(:group) - - group_1.add_owner(user_1) - group_1.add_owner(user_2) - group_2.add_owner(user_1) - - expect(described_class.count_users_by_group_id).to eq(group_1.id => 2, - group_2.id => 1) - end - end - - describe '.count_users_by_group_id with optimized_count_users_by_group_id feature flag on' do - before do - stub_feature_flags(optimized_count_users_by_group_id: true) - end - - it_behaves_like '.count_users_by_group_id' - - it 'does not JOIN users' do - scope = described_class.all - expect(scope).not_to receive(:joins).with(:user) - - scope.count_users_by_group_id - end - end - - describe '.count_users_by_group_id with optimized_count_users_by_group_id feature flag off' do - before do - stub_feature_flags(optimized_count_users_by_group_id: false) - end - - it_behaves_like '.count_users_by_group_id' - - it 'does JOIN users' do - scope = described_class.all - expect(scope).to receive(:joins).with(:user).and_call_original - - scope.count_users_by_group_id - end + it 'counts users by group ID' do + user_1 = create(:user) + user_2 = create(:user) + group_1 = create(:group) + group_2 = create(:group) + + group_1.add_owner(user_1) + group_1.add_owner(user_2) + group_2.add_owner(user_1) + + expect(described_class.count_users_by_group_id).to eq(group_1.id => 2, + group_2.id => 1) end describe '.of_ldap_type' do diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb new file mode 100644 index 00000000000..257d78dfa2c --- /dev/null +++ b/spec/models/namespace_setting_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe NamespaceSetting, type: :model do + it { is_expected.to belong_to(:namespace) } +end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4d137f60f9a..ad4c8448745 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Namespace do it { is_expected.to have_many :children } it { is_expected.to have_one :root_storage_statistics } it { is_expected.to have_one :aggregation_schedule } + it { is_expected.to have_one :namespace_settings } it { is_expected.to have_many :custom_emoji } end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 69e15c22897..fa2e4b63648 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3711,6 +3711,12 @@ RSpec.describe User do expect(user.namespace).not_to be_nil end + + it 'creates the namespace setting' do + user.save! + + expect(user.namespace.namespace_settings).to be_persisted + end end context 'for an existing user' do diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb index cf7dc7ede51..ad67f737725 100644 --- a/spec/requests/api/group_import_spec.rb +++ b/spec/requests/api/group_import_spec.rb @@ -122,6 +122,7 @@ RSpec.describe API::GroupImport do before do allow_next_instance_of(Group) do |group| allow(group).to receive(:persisted?).and_return(false) + allow(group).to receive(:save).and_return(false) end end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index bbce5b4cfb6..23889912d7a 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -321,6 +321,26 @@ RSpec.describe API::Members do expect(response).to have_gitlab_http_status(:bad_request) end end + + context 'adding project bot' do + let_it_be(:project_bot) { create(:user, :project_bot) } + + before do + unrelated_project = create(:project) + unrelated_project.add_maintainer(project_bot) + end + + it 'returns 400' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), + params: { user_id: project_bot.id, access_level: Member::DEVELOPER } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']['user_id']).to( + include('project bots cannot be added to other groups / projects')) + end.not_to change { project.members.count } + end + end end shared_examples 'PUT /:source_type/:id/members/:user_id' do |source_type| @@ -461,8 +481,34 @@ RSpec.describe API::Members do end end - it_behaves_like 'POST /:source_type/:id/members', 'project' do - let(:source) { project } + describe 'POST /projects/:id/members' do + it_behaves_like 'POST /:source_type/:id/members', 'project' do + let(:source) { project } + end + + context 'adding owner to project' do + it 'returns 403' do + expect do + post api("/projects/#{project.id}/members", maintainer), + params: { user_id: stranger.id, access_level: Member::OWNER } + + expect(response).to have_gitlab_http_status(:bad_request) + end.not_to change { project.members.count } + end + end + + context 'remove bot from project' do + it 'returns a 403 forbidden' do + project_bot = create(:user, :project_bot) + create(:project_member, project: project, user: project_bot) + + expect do + delete api("/projects/#{project.id}/members/#{project_bot.id}", maintainer) + + expect(response).to have_gitlab_http_status(:forbidden) + end.not_to change { project.members.count } + end + end end it_behaves_like 'POST /:source_type/:id/members', 'group' do @@ -484,28 +530,4 @@ RSpec.describe API::Members do it_behaves_like 'DELETE /:source_type/:id/members/:user_id', 'group' do let(:source) { group } end - - context 'Adding owner to project' do - it 'returns 403' do - expect do - post api("/projects/#{project.id}/members", maintainer), - params: { user_id: stranger.id, access_level: Member::OWNER } - - expect(response).to have_gitlab_http_status(:bad_request) - end.to change { project.members.count }.by(0) - end - end - - context 'remove bot from project' do - it 'returns a 403 forbidden' do - project_bot = create(:user, :project_bot) - create(:project_member, project: project, user: project_bot) - - expect do - delete api("/projects/#{project.id}/members/#{project_bot.id}", maintainer) - - expect(response).to have_gitlab_http_status(:forbidden) - end.to change { project.members.count }.by(0) - end - end end diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index b238949ce47..507e9fa6710 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' RSpec.describe API::ProjectMilestones do - let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace ) } - let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } - let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, namespace: user.namespace ) } + let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') } + let_it_be(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') } before do project.add_developer(user) @@ -16,6 +16,65 @@ RSpec.describe API::ProjectMilestones do let(:route) { "/projects/#{project.id}/milestones" } end + describe 'GET /projects/:id/milestones' do + context 'when include_parent_milestones is true' do + let_it_be(:group) { create(:group, :public) } + let_it_be(:child_group) { create(:group, :public, parent: group) } + let_it_be(:child_project) { create(:project, group: child_group) } + let_it_be(:project_milestone) { create(:milestone, project: child_project) } + let_it_be(:group_milestone) { create(:milestone, group: group) } + let_it_be(:child_group_milestone) { create(:milestone, group: child_group) } + + before do + child_project.add_developer(user) + end + + it 'includes parent groups milestones' do + milestones = [child_group_milestone, group_milestone, project_milestone] + + get api("/projects/#{child_project.id}/milestones", user), + params: { include_parent_milestones: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(3) + expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id)) + end + + context 'when user has no access to an ancestor group' do + before do + [child_group, group].each do |group| + group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + end + + it 'does not show ancestor group milestones' do + milestones = [child_group_milestone, project_milestone] + + get api("/projects/#{child_project.id}/milestones", user), + params: { include_parent_milestones: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(2) + expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id)) + end + end + + context 'when filtering by iids' do + it 'does not filter by iids' do + milestones = [child_group_milestone, group_milestone, project_milestone] + + get api("/projects/#{child_project.id}/milestones", user), + params: { include_parent_milestones: true, iids: [group_milestone.iid] } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(3) + + expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id)) + end + end + end + end + describe 'DELETE /projects/:id/milestones/:milestone_id' do let(:guest) { create(:user) } let(:reporter) { create(:user) } diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index b2e9db8c9de..fc877f45a39 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -129,4 +129,13 @@ RSpec.describe Groups::CreateService, '#execute' do expect { subject }.to change { ChatTeam.count }.from(0).to(1) end end + + describe 'creating a setting record' do + let(:service) { described_class.new(user, group_params) } + + it 'create the settings record connected to the group' do + group = subject + expect(group.namespace_settings).to be_persisted + end + end end -- cgit v1.2.3