diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-30 21:09:30 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-30 21:09:30 +0300 |
commit | 370736438075748c36abd7fd7dd32a8ef98048f9 (patch) | |
tree | d74dd4529092edeb7dcb914bf0311f962d89e7bb | |
parent | e7b262a4c5cf70fed6eb25ba7a0eb1336e6eb639 (diff) |
Add latest changes from gitlab-org/gitlab@master
59 files changed, 673 insertions, 149 deletions
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue index 5f4a1e44ea3..b070848cae9 100644 --- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue @@ -22,6 +22,18 @@ export default { computed: { ...mapGetters(['getNotesData', 'getNoteableData', 'noteableType', 'getCurrentUserLastNote']), }, + mounted() { + // We override the Bootstrap Vue click outside behaviour + // to allow for clicking in the autocomplete dropdowns + // without this override the submit dropdown will close + // whenever a item in the autocomplete dropdown is clicked + const originalClickOutHandler = this.$refs.dropdown.$refs.dropdown.clickOutHandler; + this.$refs.dropdown.$refs.dropdown.clickOutHandler = (e) => { + if (!e.target.closest('.atwho-container')) { + originalClickOutHandler(e); + } + }; + }, methods: { ...mapActions('batchComments', ['publishReview']), async submitReview() { @@ -52,7 +64,13 @@ export default { </script> <template> - <gl-dropdown right class="submit-review-dropdown" variant="info" category="secondary"> + <gl-dropdown + ref="dropdown" + right + class="submit-review-dropdown" + variant="info" + category="secondary" + > <template #button-content> {{ __('Finish review') }} <gl-icon class="dropdown-chevron" name="chevron-up" /> diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js index e4f6e931ec0..1973a3c91ef 100644 --- a/app/assets/javascripts/integrations/constants.js +++ b/app/assets/javascripts/integrations/constants.js @@ -18,7 +18,7 @@ export const overrideDropdownDescriptions = { }; export const I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE = s__( - 'Integrations|Connection failed. Please check your settings.', + 'Integrations|Connection failed. Check your integration settings.', ); export const I18N_DEFAULT_ERROR_MESSAGE = __('Something went wrong on our end.'); export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection successful.'); diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index 9307d7c2d3d..f1f574c6424 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -140,15 +140,24 @@ export default { this.isTesting = true; testIntegrationSettings(this.propsSource.testPath, this.getFormData()) - .then(({ data: { error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } }) => { - if (error) { - this.setIsValidated(); - this.$toast.show(message); - return; - } + .then( + ({ + data: { + error, + message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE, + service_response: serviceResponse, + }, + }) => { + if (error) { + const errorMessage = serviceResponse ? [message, serviceResponse].join(' ') : message; + this.setIsValidated(); + this.$toast.show(errorMessage); + return; + } - this.$toast.show(I18N_SUCCESSFUL_CONNECTION_MESSAGE); - }) + this.$toast.show(I18N_SUCCESSFUL_CONNECTION_MESSAGE); + }, + ) .catch((error) => { this.$toast.show(I18N_DEFAULT_ERROR_MESSAGE); Sentry.captureException(error); @@ -284,6 +293,7 @@ export default { :key="`${currentKey}-${field.name}`" v-bind="field" :is-validated="isValidated" + :data-qa-selector="`${field.name}_div`" /> </div> </div> diff --git a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue index 9e1ad24ae9f..b8fd8995744 100644 --- a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue +++ b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue @@ -33,6 +33,7 @@ export default { :key="`${currentKey}-${field.name}`" v-bind="field" :is-validated="isValidated" + :data-qa-selector="`${field.name}_div`" /> </div> </template> diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue index 327da1fb2a1..716b78b94fc 100644 --- a/app/assets/javascripts/releases/components/app_edit_new.vue +++ b/app/assets/javascripts/releases/components/app_edit_new.vue @@ -1,5 +1,13 @@ <script> -import { GlButton, GlFormCheckbox, GlFormInput, GlFormGroup, GlLink, GlSprintf } from '@gitlab/ui'; +import { + GlButton, + GlDatepicker, + GlFormCheckbox, + GlFormInput, + GlFormGroup, + GlLink, + GlSprintf, +} from '@gitlab/ui'; import { mapState, mapActions, mapGetters } from 'vuex'; import { isSameOriginUrl, getParameterByName } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -16,6 +24,7 @@ export default { GlFormInput, GlFormGroup, GlButton, + GlDatepicker, GlLink, GlSprintf, MarkdownField, @@ -31,6 +40,7 @@ export default { 'markdownDocsPath', 'markdownPreviewPath', 'editReleaseDocsPath', + 'upcomingReleaseDocsPath', 'releasesPagePath', 'release', 'newMilestonePath', @@ -76,6 +86,14 @@ export default { this.updateIncludeTagNotes(includeTagNotes); }, }, + releasedAt: { + get() { + return this.release.releasedAt; + }, + set(date) { + this.updateReleasedAt(date); + }, + }, cancelPath() { const backUrl = getParameterByName(BACK_URL_PARAM); @@ -118,6 +136,7 @@ export default { 'updateReleaseNotes', 'updateReleaseMilestones', 'updateIncludeTagNotes', + 'updateReleasedAt', ]), submitForm() { if (!this.isFormSubmissionDisabled) { @@ -166,6 +185,22 @@ export default { /> </div> </gl-form-group> + <gl-form-group :label="__('Release date')" label-for="release-released-at"> + <template #label-description> + <gl-sprintf + :message=" + __( + 'The date when the release is ready. A release with a date in the future is labeled as an %{linkStart}Upcoming Release%{linkEnd}.', + ) + " + > + <template #link="{ content }"> + <gl-link :href="upcomingReleaseDocsPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + </template> + <gl-datepicker id="release-released-at" v-model="releasedAt" :default-date="releasedAt" /> + </gl-form-group> <gl-form-group data-testid="release-notes"> <label for="release-notes">{{ __('Release notes') }}</label> <div class="bordered-box pr-3 pl-3"> diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue index 91d6d0911a4..e7eaf5a49bf 100644 --- a/app/assets/javascripts/releases/components/release_block_footer.vue +++ b/app/assets/javascripts/releases/components/release_block_footer.vue @@ -42,9 +42,9 @@ export default { default: null, }, releasedAt: { - type: String, + type: Date, required: false, - default: '', + default: null, }, }, computed: { diff --git a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql index 236d266a40a..f6ed9a8317c 100644 --- a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql +++ b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql @@ -3,6 +3,7 @@ fragment ReleaseForEditing on Release { name tagName description + releasedAt assets { links { nodes { diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js index 08197377f61..bf4400ea59a 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js @@ -246,3 +246,7 @@ export const fetchTagNotes = ({ commit, state }, tagName) => { export const updateIncludeTagNotes = ({ commit }, includeTagNotes) => { commit(types.UPDATE_INCLUDE_TAG_NOTES, includeTagNotes); }; + +export const updateReleasedAt = ({ commit }, releasedAt) => { + commit(types.UPDATE_RELEASED_AT, releasedAt); +}; diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js index 0ca5eb9931a..240e85b75f3 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js @@ -138,6 +138,7 @@ export const releaseUpdateMutatationVariables = (state, getters) => { projectPath: state.projectPath, tagName: state.release.tagName, name, + releasedAt: state.release.releasedAt, description: state.includeTagNotes ? getters.formattedReleaseNotes : state.release.description, diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js index daa077309a1..fca13138ee2 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js @@ -26,3 +26,4 @@ export const RECEIVE_TAG_NOTES_SUCCESS = 'RECEIVE_TAG_NOTES_SUCCESS'; export const RECEIVE_TAG_NOTES_ERROR = 'RECEIVE_TAG_NOTES_ERROR'; export const UPDATE_INCLUDE_TAG_NOTES = 'UPDATE_INCLUDE_TAG_NOTES'; +export const UPDATE_RELEASED_AT = 'UPDATE_RELEASED_AT'; diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js index 6b22468bbfe..e28654a8980 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js @@ -14,6 +14,7 @@ export default { description: '', milestones: [], groupMilestones: [], + releasedAt: new Date(), assets: { links: [], }, @@ -113,4 +114,7 @@ export default { [types.UPDATE_INCLUDE_TAG_NOTES](state, includeTagNotes) { state.includeTagNotes = includeTagNotes; }, + [types.UPDATE_RELEASED_AT](state, releasedAt) { + state.release.releasedAt = releasedAt; + }, }; diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/state.js b/app/assets/javascripts/releases/stores/modules/edit_new/state.js index 33cb3ee06d0..f866daa2b1e 100644 --- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js +++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js @@ -10,6 +10,7 @@ export default ({ newMilestonePath, releasesPagePath, editReleaseDocsPath, + upcomingReleaseDocsPath, tagName = null, defaultBranch = null, @@ -25,6 +26,7 @@ export default ({ newMilestonePath, releasesPagePath, editReleaseDocsPath, + upcomingReleaseDocsPath, /** * The name of the tag associated with the release, provided by the backend. diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js index 22d5fb4f620..21aad4d716e 100644 --- a/app/assets/javascripts/releases/util.js +++ b/app/assets/javascripts/releases/util.js @@ -11,10 +11,13 @@ const convertScalarProperties = (graphQLRelease) => 'tagPath', 'description', 'descriptionHtml', - 'releasedAt', 'upcomingRelease', ]); +const convertDateProperties = ({ releasedAt }) => ({ + releasedAt: new Date(releasedAt), +}); + const convertAssets = (graphQLRelease) => { let sources = []; if (graphQLRelease.assets.sources?.nodes) { @@ -88,6 +91,7 @@ const convertMilestones = (graphQLRelease) => ({ */ export const convertGraphQLRelease = (graphQLRelease) => ({ ...convertScalarProperties(graphQLRelease), + ...convertDateProperties(graphQLRelease), ...convertAssets(graphQLRelease), ...convertEvidences(graphQLRelease), ...convertLinks(graphQLRelease), diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue index c20dd3b677d..d17c8a123d5 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue @@ -72,9 +72,12 @@ export default { }, }, computed: { + isMergeRequest() { + return this.issuableType === IssuableType.MergeRequest; + }, cannotMerge() { const canMerge = this.user.mergeRequestInteraction?.canMerge || this.user.can_merge; - return this.issuableType === IssuableType.MergeRequest && !canMerge; + return this.isMergeRequest && !canMerge; }, tooltipTitle() { const { name = '', availability = '' } = this.user; @@ -86,6 +89,10 @@ export default { }); }, tooltipOption() { + if (this.isMergeRequest) { + return null; + } + return { container: 'body', placement: this.tooltipPlacement, @@ -96,6 +103,10 @@ export default { return this.user.web_url || this.user.webUrl; }, assigneeId() { + if (this.isMergeRequest) { + return null; + } + return isGid(this.user.id) ? getIdFromGraphQLId(this.user.id) : this.user.id; }, }, @@ -105,6 +116,7 @@ export default { <template> <!-- must be `d-inline-block` or parent flex-basis causes width issues --> <gl-link + v-gl-tooltip="tooltipOption" :href="assigneeUrl" :title="tooltipTitle" :data-user-id="assigneeId" diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue index 36a08482e69..c9b0a4ae2b3 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue @@ -68,10 +68,9 @@ export default { <template> <!-- must be `d-inline-block` or parent flex-basis causes width issues --> <gl-link + v-gl-tooltip="tooltipOption" :href="reviewerUrl" :title="tooltipTitle" - :data-user-id="user.id" - data-placement="left" class="gl-display-inline-block js-user-link" > <!-- use d-flex so that slot can be appropriately styled --> diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index eeffc4fc21b..1e921b4234e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -432,7 +432,6 @@ $gl-input-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top: 10px; $gl-sidebar-padding: 22px; -$gl-bar-padding: 3px; $input-horizontal-padding: 12px; $browser-scrollbar-size: 10px; diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb index 443274608e3..cee9e9feb7b 100644 --- a/app/controllers/projects/settings/integrations_controller.rb +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -89,7 +89,7 @@ module Projects unless result[:success] return { error: true, - message: s_('Integrations|Connection failed. Please check your settings.'), + message: s_('Integrations|Connection failed. Check your integration settings.'), service_response: result[:message].to_s, test_failed: true } @@ -99,7 +99,7 @@ module Projects rescue *Gitlab::HTTP::HTTP_ERRORS => e { error: true, - message: s_('Integrations|Connection failed. Please check your settings.'), + message: s_('Integrations|Connection failed. Check your integration settings.'), service_response: e.message, test_failed: true } diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb index a516ac85131..0fe60c37b85 100644 --- a/app/helpers/releases_helper.rb +++ b/app/helpers/releases_helper.rb @@ -81,7 +81,8 @@ module ReleasesHelper release_assets_docs_path: releases_help_page_path(anchor: 'release-assets'), manage_milestones_path: project_milestones_path(@project), new_milestone_path: new_project_milestone_path(@project), - edit_release_docs_path: releases_help_page_path(anchor: 'edit-a-release') + edit_release_docs_path: releases_help_page_path(anchor: 'edit-a-release'), + upcoming_release_docs_path: releases_help_page_path(anchor: 'upcoming-releases') } end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 8529959f73c..c4cfd3b2287 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -66,7 +66,13 @@ module TodosHelper return _('design') if todo.for_design? return _('alert') if todo.for_alert? - todo.target_type.titleize.downcase + target_type = if todo.for_issue_or_work_item? + todo.target.issue_type + else + todo.target_type + end + + target_type.titleize.downcase end def todo_target_path(todo) @@ -80,6 +86,8 @@ module TodosHelper todos_design_path(todo, path_options) elsif todo.for_alert? details_project_alert_management_path(todo.project, todo.target) + elsif todo.for_issue_or_work_item? + Gitlab::UrlBuilder.build(todo.target, only_path: true) else path = [todo.resource_parent, todo.target] diff --git a/app/models/todo.rb b/app/models/todo.rb index 45ab770a0f6..cff7a93f72f 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -230,6 +230,10 @@ class Todo < ApplicationRecord target_type == AlertManagement::Alert.name end + def for_issue_or_work_item? + [Issue.name, WorkItem.name].any? { |klass_name| target_type == klass_name } + end + # override to return commits, which are not active record def target if for_commit? diff --git a/db/post_migrate/20220630050050_index_vulnerability_reads_on_casted_cluster_agent_id_full.rb b/db/post_migrate/20220630050050_index_vulnerability_reads_on_casted_cluster_agent_id_full.rb new file mode 100644 index 00000000000..58b6342e30f --- /dev/null +++ b/db/post_migrate/20220630050050_index_vulnerability_reads_on_casted_cluster_agent_id_full.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class IndexVulnerabilityReadsOnCastedClusterAgentIdFull < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_vuln_reads_on_casted_cluster_agent_id_where_it_is_null' + + def up + add_concurrent_index :vulnerability_reads, + :casted_cluster_agent_id, + name: INDEX_NAME, + where: 'casted_cluster_agent_id IS NOT NULL' + end + + def down + remove_concurrent_index_by_name :vulnerability_reads, INDEX_NAME + end +end diff --git a/db/schema_migrations/20220630050050 b/db/schema_migrations/20220630050050 new file mode 100644 index 00000000000..2ec998847eb --- /dev/null +++ b/db/schema_migrations/20220630050050 @@ -0,0 +1 @@ +dfb314ef76efc54a2464e6b84e71753caf58bc8508f9e64b403066ea4847fe56
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 817e0795f2f..8ee7d12ede8 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29909,6 +29909,8 @@ COMMENT ON INDEX index_verification_codes_on_phone_and_visitor_id_code IS 'JiHu- CREATE UNIQUE INDEX index_vuln_historical_statistics_on_project_id_and_date ON vulnerability_historical_statistics USING btree (project_id, date); +CREATE INDEX index_vuln_reads_on_casted_cluster_agent_id_where_it_is_null ON vulnerability_reads USING btree (casted_cluster_agent_id) WHERE (casted_cluster_agent_id IS NOT NULL); + CREATE INDEX index_vuln_reads_on_project_id_state_severity_and_vuln_id ON vulnerability_reads USING btree (project_id, state, severity, vulnerability_id DESC); CREATE INDEX index_vulnerabilites_common_finder_query ON vulnerabilities USING btree (project_id, state, report_type, severity, id); diff --git a/doc/api/usage_data.md b/doc/api/usage_data.md index 6e50794a0ac..9a94c8fd12a 100644 --- a/doc/api/usage_data.md +++ b/doc/api/usage_data.md @@ -34,7 +34,7 @@ Example response: by month product_section: enablement product_stage: enablement - product_group: group::global search + product_group: global_search product_category: global_search value_type: number status: active diff --git a/doc/development/service_ping/metrics_dictionary.md b/doc/development/service_ping/metrics_dictionary.md index dd201678e51..58cf95d12c3 100644 --- a/doc/development/service_ping/metrics_dictionary.md +++ b/doc/development/service_ping/metrics_dictionary.md @@ -207,7 +207,7 @@ description: GitLab instance unique identifier product_category: collection product_section: growth product_stage: growth -product_group: group::product intelligence +product_group: product_intelligence value_type: string status: active milestone: 9.1 diff --git a/lib/gitlab/database/background_migration/batched_migration_runner.rb b/lib/gitlab/database/background_migration/batched_migration_runner.rb index d15886f02b8..c93e0c462b5 100644 --- a/lib/gitlab/database/background_migration/batched_migration_runner.rb +++ b/lib/gitlab/database/background_migration/batched_migration_runner.rb @@ -76,7 +76,10 @@ module Gitlab run_migration_while(migration, :finalizing) - raise FailedToFinalize unless migration.finished? + error_message = "Batched migration #{migration.job_class_name} could not be completed and a manual action is required."\ + "Check the admin panel at (`/admin/background_migrations`) for more details." + + raise FailedToFinalize, error_message unless migration.finished? end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8541420acc3..108ad5b925e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18137,7 +18137,7 @@ msgstr "" msgid "GroupActivityMetrics|Issues created" msgstr "" -msgid "GroupActivityMetrics|Last 90 days" +msgid "GroupActivityMetrics|Last 30 days" msgstr "" msgid "GroupActivityMetrics|Members added" @@ -20766,7 +20766,7 @@ msgstr "" msgid "Integrations|Connection details" msgstr "" -msgid "Integrations|Connection failed. Please check your settings." +msgid "Integrations|Connection failed. Check your integration settings." msgstr "" msgid "Integrations|Connection successful." @@ -31795,6 +31795,9 @@ msgstr "" msgid "Release assets documentation" msgstr "" +msgid "Release date" +msgstr "" + msgid "Release does not have the same project as the milestone" msgstr "" @@ -38397,6 +38400,9 @@ msgstr "" msgid "The data source is connected, but there is no data to display. %{documentationLink}" msgstr "" +msgid "The date when the release is ready. A release with a date in the future is labeled as an %{linkStart}Upcoming Release%{linkEnd}." +msgstr "" + msgid "The default CI/CD configuration file and path for new projects." msgstr "" diff --git a/package.json b/package.json index e33699b0402..34027119cbc 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,7 @@ "sortablejs": "^1.10.2", "string-hash": "1.1.3", "style-loader": "^2.0.0", - "swagger-ui-dist": "4.8.0", + "swagger-ui-dist": "4.12.0", "three": "^0.84.0", "three-orbit-controls": "^82.1.0", "three-stl-loader": "^1.0.4", diff --git a/qa/qa/page/project/settings/integrations.rb b/qa/qa/page/project/settings/integrations.rb index 420dcb63918..0d5515aacdf 100644 --- a/qa/qa/page/project/settings/integrations.rb +++ b/qa/qa/page/project/settings/integrations.rb @@ -8,12 +8,17 @@ module QA view 'app/assets/javascripts/integrations/index/components/integrations_table.vue' do element :prometheus_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern element :jira_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern + element :pipelines_email_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern end def click_on_prometheus_integration click_element :prometheus_link end + def click_pipelines_email_link + click_element :pipelines_email_link + end + def click_jira_link click_element :jira_link end diff --git a/qa/qa/page/project/settings/services/pipeline_status_emails.rb b/qa/qa/page/project/settings/services/pipeline_status_emails.rb new file mode 100644 index 00000000000..2f78577e3d5 --- /dev/null +++ b/qa/qa/page/project/settings/services/pipeline_status_emails.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + module Services + class PipelineStatusEmails < QA::Page::Base + view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do + element :recipients_div, %q(:data-qa-selector="`${field.name}_div`") # rubocop:disable QA/ElementWithPattern + element :notify_only_broken_pipelines_div, %q(:data-qa-selector="`${field.name}_div`") # rubocop:disable QA/ElementWithPattern + element :save_changes_button + end + + def set_recipients(emails) + within_element :recipients_div do + fill_in 'Recipients', with: emails.join(',') + end + end + + def toggle_notify_broken_pipelines + within_element :notify_only_broken_pipelines_div do + uncheck 'Notify only broken pipelines', allow_label_click: true + end + end + + def click_save_button + click_element(:save_changes_button) + end + end + end + end + end + end +end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb index 1b848feb50d..2fb8b18b71f 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -104,12 +104,6 @@ module QA false end - def api_delete - super - - QA::Runtime::Logger.debug("Deleted user '#{username}'") - end - def api_delete_path "/users/#{id}?hard_delete=#{hard_delete_on_api_removal}" rescue NoValueError diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb index 44cae31f5d8..b1d59b90e9c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb @@ -2,49 +2,45 @@ module QA RSpec.describe 'Manage' do - describe 'Check for broken images', :requires_admin do - before(:context) do - @api_client = Runtime::API::Client.as_admin - @new_user = Resource::User.fabricate_via_api! do |user| - user.api_client = @api_client - end - @new_admin = Resource::User.fabricate_via_api! do |user| - user.admin = true - user.api_client = @api_client - end + shared_examples 'loads all images' do |admin| + let(:api_client) { Runtime::API::Client.as_admin } - Page::Main::Menu.perform(&:sign_out_if_signed_in) + let(:user) do + Resource::User.fabricate_via_api! do |resource| + resource.admin = admin + resource.api_client = api_client + end end - after(:context) do - @new_user.remove_via_api! - @new_admin.remove_via_api! + after do + user.remove_via_api! end - shared_examples 'loads all images' do - it 'loads all images' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: new_user) } + it 'loads all images' do + Flow::Login.sign_in(as: user) - Page::Dashboard::Welcome.perform do |welcome| - expect(welcome).to have_welcome_title("Welcome to GitLab") + Page::Dashboard::Welcome.perform do |welcome| + expect(welcome).to have_welcome_title("Welcome to GitLab") - # This would be better if it were a visual validation test - expect(welcome).to have_loaded_all_images - end + # This would be better if it were a visual validation test + expect(welcome).to have_loaded_all_images end end + end - context 'when logged in as a new user', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347885' do - it_behaves_like 'loads all images' do - let(:new_user) { @new_user } - end + describe 'Check for broken images', :requires_admin, :reliable do + context( + 'when logged in as a new user', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347885' + ) do + it_behaves_like 'loads all images', false end - context 'when logged in as a new admin', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347884' do - it_behaves_like 'loads all images' do - let(:new_user) { @new_admin } - end + context( + 'when logged in as a new admin', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347884' + ) do + it_behaves_like 'loads all images', true end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb index f41e5985622..b815186cd49 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb @@ -46,19 +46,22 @@ module QA total = mailhog_data.dig('total') subjects = mailhog_data.dig('items') .map(&method(:mailhog_item_subject)) - .join("\n") Runtime::Logger.debug(%Q[Total number of emails: #{total}]) - Runtime::Logger.debug(%Q[Subjects:\n#{subjects}]) + Runtime::Logger.debug(%Q[Subjects:\n#{subjects.join("\n")}]) # Expect at least two invitation messages: group and project - mailhog_data if total >= 2 + mailhog_data if mailhog_project_message_count(subjects) >= 1 end end def mailhog_item_subject(item) item.dig('Content', 'Headers', 'Subject', 0) end + + def mailhog_project_message_count(subjects) + subjects.count { |subject| subject.include?('project was granted') } + end end end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb new file mode 100644 index 00000000000..f4794b3a904 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module QA + RSpec.shared_examples 'notifies on a pipeline' do |exit_code| + before do + push_commit(exit_code: exit_code) + end + + it 'sends an email' do + meta = exit_code_meta(exit_code) + + project.visit! + Flow::Pipeline.wait_for_latest_pipeline(status: meta[:status]) + + messages = mail_hog_messages(mail_hog) + subjects = messages.map(&:subject) + targets = messages.map(&:to) + + aggregate_failures do + expect(subjects).to include(meta[:email_subject]) + expect(subjects).to include(/#{Regexp.escape(project.name)}/) + expect(targets).to include(*emails) + end + end + end + + RSpec.describe 'Verify', :orchestrated, :runner, :requires_admin, :smtp do + describe 'Pipeline status emails' do + let(:executor) { "qa-runner-#{Time.now.to_i}" } + let(:emails) { %w[foo@bar.com baz@buzz.com] } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'pipeline-status-project' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + let(:mail_hog) { Vendor::MailHog::API.new } + + before(:all) do + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) + end + + before do + setup_pipeline_emails(emails) + end + + describe 'when pipeline passes', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366240' do + include_examples 'notifies on a pipeline', 0 + end + + describe 'when pipeline fails', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366241' do + include_examples 'notifies on a pipeline', 1 + end + + def push_commit(exit_code: 0) + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: gitlab_ci_yaml(exit_code: exit_code) + } + ] + ) + end + end + + def setup_pipeline_emails(emails) + page.visit Runtime::Scenario.gitlab_address + Flow::Login.sign_in_unless_signed_in + + project.visit! + + Page::Project::Menu.perform(&:go_to_integrations_settings) + QA::Page::Project::Settings::Integrations.perform(&:click_pipelines_email_link) + + QA::Page::Project::Settings::Services::PipelineStatusEmails.perform do |pipeline_status_emails| + pipeline_status_emails.toggle_notify_broken_pipelines # notify on pass and fail + pipeline_status_emails.set_recipients(emails) + pipeline_status_emails.click_save_button + end + end + + def gitlab_ci_yaml(exit_code: 0, tag: executor) + <<~YAML + test-pipeline-email: + tags: + - #{tag} + script: sleep 5; exit #{exit_code}; + YAML + end + + private + + def exit_code_meta(exit_code) + { + 0 => { status: 'passed', email_subject: /Successful pipeline/ }, + 1 => { status: 'failed', email_subject: /Failed pipeline/ } + }[exit_code] + end + + def mail_hog_messages(mail_hog_api) + Support::Retrier.retry_until(sleep_interval: 1) do + Runtime::Logger.debug('Fetching email...') + + messages = mail_hog_api.fetch_messages + logs = messages.map { |m| "#{m.to}: #{m.subject}" } + + Runtime::Logger.debug("MailHog Logs: #{logs.join("\n")}") + + # for failing pipelines we have three messages + # one for the owner + # and one for each recipient + messages if mail_hog_pipeline_count(messages) >= 2 + end + end + + def mail_hog_pipeline_count(messages) + messages.count { |message| message.subject.include?('pipeline') } + end + end + end +end diff --git a/qa/qa/vendor/mail_hog/api.rb b/qa/qa/vendor/mail_hog/api.rb new file mode 100644 index 00000000000..85eb0631624 --- /dev/null +++ b/qa/qa/vendor/mail_hog/api.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module QA + module Vendor + module MailHog + # Represents a Set of messages from a MailHog response + class Messages + include Enumerable + + attr_reader :data + + def initialize(data) + @data = data + end + + def total + data.dig('total') + end + + def each + data.dig('items')&.each do |item| + yield MessageItem.new(item) + end + end + end + + # Represents an email item from a MailHog response + class MessageItem + attr_reader :data + + def initialize(data) + @data = data + end + + def to + data.dig('Content', 'Headers', 'To', 0) + end + + def subject + data.dig('Content', 'Headers', 'Subject', 0) + end + end + + class API + include Support::API + + attr_reader :hostname + + def initialize(hostname: QA::Runtime::Env.mailhog_hostname || 'localhost') + @hostname = hostname + end + + def base_url + "http://#{hostname}:8025" + end + + def api_messages_url(version: 2) + "#{base_url}/api/v#{version}/messages" + end + + def delete_messages + delete(api_messages_url(version: 1)) + end + + def fetch_messages + Messages.new(JSON.parse(fetch_messages_json)) + end + + def fetch_messages_json + get(api_messages_url).body + end + end + end + end +end diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb index 847d3bf8956..8ee9f22aa7f 100644 --- a/spec/controllers/projects/settings/integrations_controller_spec.rb +++ b/spec/controllers/projects/settings/integrations_controller_spec.rb @@ -148,7 +148,7 @@ RSpec.describe Projects::Settings::IntegrationsController do expect(response).to be_successful expect(json_response).to eq( 'error' => true, - 'message' => 'Connection failed. Please check your settings.', + 'message' => 'Connection failed. Check your integration settings.', 'service_response' => '', 'test_failed' => true ) @@ -163,7 +163,7 @@ RSpec.describe Projects::Settings::IntegrationsController do expect(response).to be_successful expect(json_response).to eq( 'error' => true, - 'message' => 'Connection failed. Please check your settings.', + 'message' => 'Connection failed. Check your integration settings.', 'service_response' => "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed", 'test_failed' => true ) @@ -177,7 +177,7 @@ RSpec.describe Projects::Settings::IntegrationsController do expect(response).to be_successful expect(json_response).to eq( 'error' => true, - 'message' => 'Connection failed. Please check your settings.', + 'message' => 'Connection failed. Check your integration settings.', 'service_response' => 'Connection refused', 'test_failed' => true ) diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index a7478ce2657..ccf835fe56e 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -3,8 +3,13 @@ FactoryBot.define do factory :key do title - key { SSHData::PrivateKey::RSA.generate(1024, unsafe_allow_small_key: true).public_key.openssh(comment: 'dummy@gitlab.com') } - + key do + # Larger keys take longer to generate, and since this factory gets called frequently, + # let's only create the smallest one we need. + SSHData::PrivateKey::RSA.generate( + ::Gitlab::SSHPublicKey.supported_sizes(:rsa).min, unsafe_allow_small_key: true + ).public_key.openssh(comment: 'dummy@gitlab.com') + end trait :expired do to_create { |key| key.save!(validate: false) } expires_at { 2.days.ago } @@ -16,7 +21,7 @@ FactoryBot.define do end factory :key_without_comment do - key { SSHData::PrivateKey::RSA.generate(1024, unsafe_allow_small_key: true).public_key.openssh } + key { SSHData::PrivateKey::RSA.generate(3072, unsafe_allow_small_key: true).public_key.openssh } end factory :deploy_key, class: 'DeployKey' diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb index 18bb03f4617..bc88b90a2dd 100644 --- a/spec/features/admin/users/user_spec.rb +++ b/spec/features/admin/users/user_spec.rb @@ -372,8 +372,8 @@ RSpec.describe 'Admin::Users::User' do describe 'show user keys', :js do it do - key1 = create(:key, user: user, title: 'ssh-rsa Key1', key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1') - key2 = create(:key, user: user, title: 'ssh-rsa Key2', key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2') + key1 = create(:key, user: user, title: 'ssh-rsa Key1') + key2 = create(:key, user: user, title: 'ssh-rsa Key2') visit admin_user_path(user) diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index adb43d60306..e02cd182b2c 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -60,6 +60,21 @@ RSpec.describe 'Dashboard Todos' do end end + context 'when todo references an issue of type task' do + let(:task) { create(:issue, :task, project: project) } + let!(:task_todo) { create(:todo, :mentioned, user: user, project: project, target: task, author: author) } + + before do + sign_in(user) + + visit dashboard_todos_path + end + + it 'displays the correct issue type name' do + expect(page).to have_content('mentioned you on task') + end + end + context 'user has an unauthorized todo' do before do sign_in(user) @@ -85,6 +100,10 @@ RSpec.describe 'Dashboard Todos' do visit dashboard_todos_path end + it 'displays the correct issue type name' do + expect(page).to have_content('mentioned you on issue') + end + it 'has todo present' do expect(page).to have_selector('.todos-list .todo', count: 1) end diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js index a2bdece821f..21e57a2e33c 100644 --- a/spec/frontend/integrations/edit/components/integration_form_spec.js +++ b/spec/frontend/integrations/edit/components/integration_form_spec.js @@ -596,37 +596,42 @@ describe('IntegrationForm', () => { }); describe.each` - scenario | replyStatus | errorMessage | expectToast | expectSentry - ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true} - ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false} - ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false} - `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => { - beforeEach(async () => { - mockAxios.onPut(mockTestPath).replyOnce(replyStatus, { - error: Boolean(errorMessage), - message: errorMessage, + scenario | replyStatus | errorMessage | serviceResponse | expectToast | expectSentry + ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true} + ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${undefined} | ${'an error'} | ${false} + ${'when "test settings" returns an error with details'} | ${httpStatus.OK} | ${'an error.'} | ${'extra info'} | ${'an error. extra info'} | ${false} + ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false} + `( + '$scenario', + ({ replyStatus, errorMessage, serviceResponse, expectToast, expectSentry }) => { + beforeEach(async () => { + mockAxios.onPut(mockTestPath).replyOnce(replyStatus, { + error: Boolean(errorMessage), + message: errorMessage, + service_response: serviceResponse, + }); + + await findTestButton().vm.$emit('click', new Event('click')); + await waitForPromises(); }); - await findTestButton().vm.$emit('click', new Event('click')); - await waitForPromises(); - }); - - it(`calls toast with '${expectToast}'`, () => { - expect(mockToastShow).toHaveBeenCalledWith(expectToast); - }); + it(`calls toast with '${expectToast}'`, () => { + expect(mockToastShow).toHaveBeenCalledWith(expectToast); + }); - it('sets `loading` prop of test button to `false`', () => { - expect(findTestButton().props('loading')).toBe(false); - }); + it('sets `loading` prop of test button to `false`', () => { + expect(findTestButton().props('loading')).toBe(false); + }); - it('sets save button `disabled` prop to `false`', () => { - expect(findProjectSaveButton().props('disabled')).toBe(false); - }); + it('sets save button `disabled` prop to `false`', () => { + expect(findProjectSaveButton().props('disabled')).toBe(false); + }); - it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => { - expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0); - }); - }); + it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => { + expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0); + }); + }, + ); }); }); diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap index fd2a8eec4d4..10d250c5ebb 100644 --- a/spec/frontend/releases/__snapshots__/util_spec.js.snap +++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap @@ -57,7 +57,7 @@ Object { "evidences": Array [], "milestones": Array [], "name": "The second release", - "releasedAt": "2019-01-10T00:00:00Z", + "releasedAt": 2019-01-10T00:00:00.000Z, "tagName": "v1.2", "tagPath": "/releases-namespace/releases-project/-/tags/v1.2", "upcomingRelease": true, @@ -188,7 +188,7 @@ Object { }, ], "name": "The first release", - "releasedAt": "2018-12-10T00:00:00Z", + "releasedAt": 2018-12-10T00:00:00.000Z, "tagName": "v1.1", "tagPath": "/releases-namespace/releases-project/-/tags/v1.1", "upcomingRelease": true, @@ -267,6 +267,7 @@ Object { }, ], "name": "The first release", + "releasedAt": 2018-12-10T00:00:00.000Z, "tagName": "v1.1", }, } @@ -400,7 +401,7 @@ Object { }, ], "name": "The first release", - "releasedAt": "2018-12-10T00:00:00Z", + "releasedAt": 2018-12-10T00:00:00.000Z, "tagName": "v1.1", "tagPath": "/releases-namespace/releases-project/-/tags/v1.1", "upcomingRelease": true, diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js index 80be27c92ff..a32001ed72b 100644 --- a/spec/frontend/releases/components/app_edit_new_spec.js +++ b/spec/frontend/releases/components/app_edit_new_spec.js @@ -1,21 +1,23 @@ -import { mount } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { merge } from 'lodash'; import Vuex from 'vuex'; import { nextTick } from 'vue'; -import { GlFormCheckbox } from '@gitlab/ui'; -import originalRelease from 'test_fixtures/api/releases/release.json'; +import { GlDatepicker, GlFormCheckbox } from '@gitlab/ui'; +import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json'; +import { convertOneReleaseGraphQLResponse } from '~/releases/util'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; -import * as commonUtils from '~/lib/utils/common_utils'; import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue'; import AssetLinksForm from '~/releases/components/asset_links_form.vue'; import { BACK_URL_PARAM } from '~/releases/constants'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.release; const originalMilestones = originalRelease.milestones; const releasesPagePath = 'path/to/releases/page'; +const upcomingReleaseDocsPath = 'path/to/upcoming/release/docs'; describe('Release edit/new component', () => { let wrapper; @@ -33,6 +35,7 @@ describe('Release edit/new component', () => { projectId: '8', groupId: '42', groupMilestonesAvailable: true, + upcomingReleaseDocsPath, }; actions = { @@ -68,7 +71,7 @@ describe('Release edit/new component', () => { ), ); - wrapper = mount(ReleaseEditNewApp, { + wrapper = mountExtended(ReleaseEditNewApp, { store, provide: { glFeatures: featureFlags, @@ -88,7 +91,7 @@ describe('Release edit/new component', () => { mock.onGet('/api/v4/projects/8/milestones').reply(200, originalMilestones); - release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true }); + release = convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse).data; }); afterEach(() => { @@ -128,6 +131,18 @@ describe('Release edit/new component', () => { expect(wrapper.find('#release-title').element.value).toBe(release.name); }); + it('renders the released at date in the "Released at" datepicker', () => { + expect(wrapper.findComponent(GlDatepicker).props('value')).toBe(release.releasedAt); + }); + + it('links to the documentation on upcoming releases in the "Released at" description', () => { + const link = wrapper.findByRole('link', { name: 'Upcoming Release' }); + + expect(link.exists()).toBe(true); + + expect(link.attributes('href')).toBe(upcomingReleaseDocsPath); + }); + it('renders the release notes in the "Release notes" textarea', () => { expect(wrapper.find('#release-notes').element.value).toBe(release.description); }); diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js index b095e9e1d78..848e802df4b 100644 --- a/spec/frontend/releases/components/release_block_footer_spec.js +++ b/spec/frontend/releases/components/release_block_footer_spec.js @@ -2,14 +2,16 @@ import { GlLink, GlIcon } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { cloneDeep } from 'lodash'; import { nextTick } from 'vue'; -import originalRelease from 'test_fixtures/api/releases/release.json'; +import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json'; +import { convertOneReleaseGraphQLResponse } from '~/releases/util'; import { trimText } from 'helpers/text_helper'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue'; // TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883 const MONTHS_IN_MS = 1000 * 60 * 60 * 24 * 31; -const mockFutureDate = new Date(new Date().getTime() + MONTHS_IN_MS).toISOString(); +const mockFutureDate = new Date(new Date().getTime() + MONTHS_IN_MS); + +const originalRelease = convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse).data; describe('Release block footer', () => { let wrapper; @@ -18,7 +20,7 @@ describe('Release block footer', () => { const factory = async (props = {}) => { wrapper = mount(ReleaseBlockFooter, { propsData: { - ...convertObjectPropsToCamelCase(release, { deep: true }), + ...originalRelease, ...props, }, }); @@ -55,8 +57,8 @@ describe('Release block footer', () => { const commitLink = commitInfoSectionLink(); expect(commitLink.exists()).toBe(true); - expect(commitLink.text()).toBe(release.commit.short_id); - expect(commitLink.attributes('href')).toBe(release.commit_path); + expect(commitLink.text()).toBe(release.commit.shortId); + expect(commitLink.attributes('href')).toBe(release.commitPath); }); it('renders the tag icon', () => { @@ -70,8 +72,8 @@ describe('Release block footer', () => { const commitLink = tagInfoSection().find(GlLink); expect(commitLink.exists()).toBe(true); - expect(commitLink.text()).toBe(release.tag_name); - expect(commitLink.attributes('href')).toBe(release.tag_path); + expect(commitLink.text()).toBe(release.tagName); + expect(commitLink.attributes('href')).toBe(release.tagPath); }); it('renders the author and creation time info', () => { @@ -114,14 +116,14 @@ describe('Release block footer', () => { const avatarImg = authorDateInfoSection().find('img'); expect(avatarImg.exists()).toBe(true); - expect(avatarImg.attributes('src')).toBe(release.author.avatar_url); + expect(avatarImg.attributes('src')).toBe(release.author.avatarUrl); }); it("renders a link to the author's profile", () => { const authorLink = authorDateInfoSection().find(GlLink); expect(authorLink.exists()).toBe(true); - expect(authorLink.attributes('href')).toBe(release.author.web_url); + expect(authorLink.attributes('href')).toBe(release.author.webUrl); }); }); @@ -138,7 +140,7 @@ describe('Release block footer', () => { it('renders the commit SHA as plain text (instead of a link)', () => { expect(commitInfoSectionLink().exists()).toBe(false); - expect(commitInfoSection().text()).toBe(release.commit.short_id); + expect(commitInfoSection().text()).toBe(release.commit.shortId); }); }); @@ -155,7 +157,7 @@ describe('Release block footer', () => { it('renders the tag name as plain text (instead of a link)', () => { expect(tagInfoSectionLink().exists()).toBe(false); - expect(tagInfoSection().text()).toBe(release.tag_name); + expect(tagInfoSection().text()).toBe(release.tagName); }); }); diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index c4910ae9b2f..17e2af687a6 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -1,7 +1,8 @@ import { mount } from '@vue/test-utils'; import $ from 'jquery'; import { nextTick } from 'vue'; -import originalRelease from 'test_fixtures/api/releases/release.json'; +import originalOneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json'; +import { convertOneReleaseGraphQLResponse } from '~/releases/util'; import * as commonUtils from '~/lib/utils/common_utils'; import * as urlUtility from '~/lib/utils/url_utility'; import EvidenceBlock from '~/releases/components/evidence_block.vue'; @@ -34,7 +35,7 @@ describe('Release block', () => { beforeEach(() => { jest.spyOn($.fn, 'renderGFM'); - release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true }); + release = convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse).data; }); afterEach(() => { diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index 41653f62ebf..c6aa67a5cd3 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -168,6 +168,15 @@ describe('Release edit/new actions', () => { }); }); + describe('updateReleasedAt', () => { + it(`commits ${types.UPDATE_RELEASED_AT} with the updated date`, () => { + const newDate = new Date(); + return testAction(actions.updateReleasedAt, newDate, state, [ + { type: types.UPDATE_RELEASED_AT, payload: newDate }, + ]); + }); + }); + describe('updateCreateFrom', () => { it(`commits ${types.UPDATE_CREATE_FROM} with the updated ref`, () => { const newRef = 'my-feature-branch'; diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js index c42c6c00f56..310e6407ada 100644 --- a/spec/frontend/releases/stores/modules/detail/getters_spec.js +++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js @@ -302,6 +302,7 @@ describe('Release edit/new getters', () => { name: 'release.name', description: 'release.description', milestones: ['release.milestone[0].title'], + releasedAt: new Date(2022, 5, 30), }, }, { @@ -310,6 +311,7 @@ describe('Release edit/new getters', () => { name: 'release.name', description: 'release.description', milestones: ['release.milestone[0].title'], + releasedAt: new Date(2022, 5, 30), }, ], [ diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js index 85844831e0b..32f0e07715d 100644 --- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js @@ -30,6 +30,7 @@ describe('Release edit/new mutations', () => { description: '', milestones: [], groupMilestones: [], + releasedAt: new Date(), assets: { links: [], }, @@ -82,6 +83,16 @@ describe('Release edit/new mutations', () => { }); }); + describe(`${types.UPDATE_RELEASED_AT}`, () => { + it("updates the release's released at date", () => { + state.release = release; + const newDate = new Date(); + mutations[types.UPDATE_RELEASED_AT](state, newDate); + + expect(state.release.releasedAt).toBe(newDate); + }); + }); + describe(`${types.UPDATE_CREATE_FROM}`, () => { it('updates the ref that the ref will be created from', () => { state.createFrom = 'main'; diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js index a286eeef14f..517b4f12559 100644 --- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js @@ -120,6 +120,7 @@ describe('AssigneeAvatarLink component', () => { it('passes the correct user id for REST API', () => { createComponent({ tooltipHasName: true, + issuableType: 'issue', user: userDataMock(), }); @@ -131,9 +132,22 @@ describe('AssigneeAvatarLink component', () => { createComponent({ tooltipHasName: true, + issuableType: 'issue', user: { ...userDataMock(), id: convertToGraphQLId(TYPE_USER, userId) }, }); expect(findUserLink().attributes('data-user-id')).toBe(String(userId)); }); + + it.each` + issuableType | userId + ${'merge_request'} | ${undefined} + ${'issue'} | ${'1'} + `('it sets data-user-id as $userId for $issuableType', ({ issuableType, userId }) => { + createComponent({ + issuableType, + }); + + expect(findUserLink().attributes('data-user-id')).toBe(userId); + }); }); diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb index b7493e84c6a..330f1c3147a 100644 --- a/spec/helpers/releases_helper_spec.rb +++ b/spec/helpers/releases_helper_spec.rb @@ -64,6 +64,7 @@ RSpec.describe ReleasesHelper do release_assets_docs_path manage_milestones_path new_milestone_path + upcoming_release_docs_path edit_release_docs_path) expect(helper.data_for_edit_release_page.keys).to match_array(keys) @@ -83,6 +84,7 @@ RSpec.describe ReleasesHelper do manage_milestones_path new_milestone_path default_branch + upcoming_release_docs_path edit_release_docs_path) expect(helper.data_for_new_release_page.keys).to match_array(keys) diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb index 922fb1d7c92..73a454fd9f7 100644 --- a/spec/helpers/todos_helper_spec.rb +++ b/spec/helpers/todos_helper_spec.rb @@ -5,7 +5,8 @@ require 'spec_helper' RSpec.describe TodosHelper do let_it_be(:user) { create(:user) } let_it_be(:author) { create(:user) } - let_it_be(:issue) { create(:issue, title: 'Issue 1') } + let_it_be(:project) { create(:project) } + let_it_be(:issue) { create(:issue, title: 'Issue 1', project: project) } let_it_be(:design) { create(:design, issue: issue) } let_it_be(:note) do create(:note, @@ -16,7 +17,7 @@ RSpec.describe TodosHelper do let_it_be(:design_todo) do create(:todo, :mentioned, user: user, - project: issue.project, + project: project, target: design, author: author, note: note) @@ -27,6 +28,15 @@ RSpec.describe TodosHelper do create(:todo, target: alert) end + let_it_be(:task_todo) do + task = create(:work_item, :task, project: project) + create(:todo, target: task, target_type: task.class.name, project: project) + end + + let_it_be(:issue_todo) do + create(:todo, target: issue) + end + describe '#todos_count_format' do it 'shows fuzzy count for 100 or more items' do expect(helper.todos_count_format(100)).to eq '99+' @@ -113,27 +123,52 @@ RSpec.describe TodosHelper do ) end end + + context 'when given a task' do + let(:todo) { task_todo } + + it 'responds with an appropriate path' do + path = helper.todo_target_path(todo) + + expect(path).to eq("/#{todo.project.full_path}/-/work_items/#{todo.target.id}") + end + end end describe '#todo_target_type_name' do + subject { helper.todo_target_type_name(todo) } + context 'when given a design todo' do let(:todo) { design_todo } - it 'responds with an appropriate target type name' do - name = helper.todo_target_type_name(todo) - - expect(name).to eq('design') - end + it { is_expected.to eq('design') } end context 'when given an alert todo' do let(:todo) { alert_todo } - it 'responds with an appropriate target type name' do - name = helper.todo_target_type_name(todo) + it { is_expected.to eq('alert') } + end + + context 'when given a task todo' do + let(:todo) { task_todo } + + it { is_expected.to eq('task') } + end + + context 'when given an issue todo' do + let(:todo) { issue_todo } + + it { is_expected.to eq('issue') } + end - expect(name).to eq('alert') + context 'when given a merge request todo' do + let(:todo) do + merge_request = create(:merge_request, source_project: project) + create(:todo, target: merge_request) end + + it { is_expected.to eq('merge request') } end end diff --git a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb index 12ed47a1025..b3157dd15fb 100644 --- a/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb +++ b/spec/lib/atlassian/jira_connect/jwt/asymmetric_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do describe '#valid?' do + let_it_be(:private_key) { OpenSSL::PKey::RSA.generate 3072 } + subject(:asymmetric_jwt) { described_class.new(jwt, verification_claims) } let(:verification_claims) { jwt_claims } @@ -12,7 +14,6 @@ RSpec.describe Atlassian::JiraConnect::Jwt::Asymmetric do let(:client_key) { '1234' } let(:public_key_id) { '123e4567-e89b-12d3-a456-426614174000' } let(:jwt_headers) { { kid: public_key_id } } - let(:private_key) { OpenSSL::PKey::RSA.generate 2048 } let(:jwt) { JWT.encode(jwt_claims, private_key, 'RS256', jwt_headers) } let(:public_key) { private_key.public_key } let(:install_keys_url) { "https://connect-install-keys.atlassian.com/#{public_key_id}" } diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb index 179e2efc0c7..1fab6807773 100644 --- a/spec/lib/gitlab/ci/jwt_spec.rb +++ b/spec/lib/gitlab/ci/jwt_spec.rb @@ -121,8 +121,8 @@ RSpec.describe Gitlab::Ci::Jwt do describe '.for_build' do shared_examples 'generating JWT for build' do context 'when signing key is present' do - let(:rsa_key) { OpenSSL::PKey::RSA.generate(1024) } - let(:rsa_key_data) { rsa_key.to_s } + let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(3072) } + let_it_be(:rsa_key_data) { rsa_key.to_s } it 'generates JWT with key id' do _payload, headers = JWT.decode(jwt, rsa_key.public_key, true, { algorithm: 'RS256' }) diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb index 97459d4a7be..d744bf7d4a6 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_runner_spec.rb @@ -380,6 +380,11 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do end context 'when migration fails to complete' do + let(:error_message) do + "Batched migration #{batched_migration.job_class_name} could not be completed and a manual action is required."\ + "Check the admin panel at (`/admin/background_migrations`) for more details." + end + it 'raises an error' do batched_migration.batched_jobs.with_status(:failed).update_all(attempts: Gitlab::Database::BackgroundMigration::BatchedJob::MAX_ATTEMPTS) @@ -390,7 +395,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do column_name, job_arguments ) - end.to raise_error described_class::FailedToFinalize + end.to raise_error(described_class::FailedToFinalize, error_message) end end end diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb index 8f0d62d8f0c..6d2026752d6 100644 --- a/spec/lib/json_web_token/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -41,7 +41,7 @@ RSpec.describe JSONWebToken::RSAToken do end context 'for invalid key to raise an exception' do - let(:new_key) { OpenSSL::PKey::RSA.generate(512) } + let(:new_key) { OpenSSL::PKey::RSA.generate(3072) } subject { JWT.decode(rsa_encoded, new_key, true, { algorithm: 'RS256' }) } diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 651e2cf273f..7df22078c6d 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -114,6 +114,26 @@ RSpec.describe Todo do end end + describe '#for_issue_or_work_item?' do + it 'returns true when target is an Issue' do + subject.target_type = 'Issue' + + expect(subject.for_issue_or_work_item?).to be_truthy + end + + it 'returns true when target is a WorkItem' do + subject.target_type = 'WorkItem' + + expect(subject.for_issue_or_work_item?).to be_truthy + end + + it 'returns false when target is not an Issue' do + subject.target_type = 'DesignManagement::Design' + + expect(subject.for_issue_or_work_item?).to be_falsey + end + end + describe '#target' do context 'for commits' do let(:project) { create(:project, :repository) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index dcf6b224009..196254d4609 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3001,7 +3001,7 @@ RSpec.describe User do it 'has all ssh keys' do user = create :user - key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id + key = create :key_without_comment, user_id: user.id expect(user.all_ssh_keys).to include(a_string_starting_with(key.key)) end diff --git a/spec/requests/jwks_controller_spec.rb b/spec/requests/jwks_controller_spec.rb index 6dbb5988f58..c9dcc238c29 100644 --- a/spec/requests/jwks_controller_spec.rb +++ b/spec/requests/jwks_controller_spec.rb @@ -18,9 +18,9 @@ RSpec.describe JwksController do end describe 'GET /-/jwks' do - let(:ci_jwt_signing_key) { OpenSSL::PKey::RSA.generate(1024) } - let(:ci_jwk) { ci_jwt_signing_key.to_jwk } - let(:oidc_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key).to_jwk } + let_it_be(:ci_jwt_signing_key) { OpenSSL::PKey::RSA.generate(3072) } + let_it_be(:ci_jwk) { ci_jwt_signing_key.to_jwk } + let_it_be(:oidc_jwk) { OpenSSL::PKey::RSA.new(Rails.application.secrets.openid_connect_signing_key).to_jwk } before do stub_application_setting(ci_jwt_signing_key: ci_jwt_signing_key.to_s) diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index f18869fb380..3be59af6a37 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true RSpec.shared_context 'container registry auth service context' do + let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(3072) } + let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } - let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } let(:payload) { JWT.decode(subject[:token], rsa_key, true, { algorithm: 'RS256' }).first } let(:authentication_abilities) do diff --git a/yarn.lock b/yarn.lock index e5b5cf166ca..056c6ac2b0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11951,10 +11951,10 @@ svg-tags@^1.0.0: resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= -swagger-ui-dist@4.8.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.8.0.tgz#5f39a038a02ffbd5defb8e1921a9ac1620d779ae" - integrity sha512-jdcO4XcbwkAtrwvHp90Usjx3d4JZMjaiS02CxBFfuSxr6G8DBXPcK471+N6BcBkwZK7VTgpUBFAyyarsAvKYFQ== +swagger-ui-dist@4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.12.0.tgz#986d90f05e81fb9db3ca40372278a5d8ce71db3a" + integrity sha512-B0Iy2ueXtbByE6OOyHTi3lFQkpPi/L7kFOKFeKTr44za7dJIELa9kzaca6GkndCgpK1QTjArnoXG+aUy0XQp1w== symbol-observable@^1.0.4: version "1.2.0" |