diff options
41 files changed, 375 insertions, 186 deletions
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js b/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js index 8a375b28797..c78266b0476 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js +++ b/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js @@ -1,8 +1,4 @@ -import { - organization, - organizationProjects, - organizationGroups, -} from 'jest/organizations/groups_and_projects/mock_data'; +import { organization, organizationProjects, organizationGroups } from '../../mock_data'; export default { Query: { diff --git a/spec/frontend/organizations/groups_and_projects/mock_data.js b/app/assets/javascripts/organizations/mock_data.js index eb829a24f50..17ab7bd1d34 100644 --- a/spec/frontend/organizations/groups_and_projects/mock_data.js +++ b/app/assets/javascripts/organizations/mock_data.js @@ -1,3 +1,9 @@ +/* eslint-disable @gitlab/require-i18n-strings */ + +// This is temporary mock data that will be removed when completing the following: +// https://gitlab.com/gitlab-org/gitlab/-/issues/420777 +// https://gitlab.com/gitlab-org/gitlab/-/issues/421441 + export const organization = { id: 'gid://gitlab/Organization/1', __typename: 'Organization', diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue index 9179331cdec..0ec8b6e2a0a 100644 --- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue +++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue @@ -6,6 +6,9 @@ const noteableTypeText = { Issue: __('issue'), Epic: __('epic'), MergeRequest: __('merge request'), + Task: __('task'), + KeyResult: __('key result'), + Objective: __('objective'), }; export default { diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue index 66ad3d50287..57faed61280 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue @@ -74,6 +74,11 @@ export default { required: false, default: false, }, + isWorkItemConfidential: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -263,6 +268,7 @@ export default { :work-item-id="workItemId" :autofocus="autofocus" :comment-button-text="commentButtonText" + :is-work-item-confidential="isWorkItemConfidential" @submitForm="updateWorkItem" @cancelEditing="cancelEditing" @error="$emit('error', $event)" diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue index b143c529014..a79169bde1e 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue @@ -3,11 +3,13 @@ import { GlButton, GlFormCheckbox, GlIcon, GlTooltipDirective } from '@gitlab/ui import { helpPagePath } from '~/helpers/help_page_helper'; import { s__, __ } from '~/locale'; import Tracking from '~/tracking'; -import { STATE_OPEN, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items/constants'; import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue'; +import CommentFieldLayout from '~/notes/components/comment_field_layout.vue'; export default { i18n: { @@ -22,6 +24,7 @@ export default { markdownDocsPath: helpPagePath('user/markdown'), }, components: { + CommentFieldLayout, GlButton, MarkdownEditor, GlFormCheckbox, @@ -89,6 +92,11 @@ export default { required: false, default: false, }, + isWorkItemConfidential: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -119,6 +127,23 @@ export default { commentButtonTextComputed() { return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText; }, + workItemDocPath() { + return this.workItemType === TASK_TYPE_NAME ? 'user/tasks.html' : 'user/okrs.html'; + }, + workItemDocAnchor() { + return this.workItemType === TASK_TYPE_NAME ? 'confidential-tasks' : 'confidential-okrs'; + }, + getWorkItemData() { + return { + confidential: this.isWorkItemConfidential, + confidential_issues_docs_path: helpPagePath(this.workItemDocPath, { + anchor: this.workItemDocAnchor, + }), + }; + }, + workItemTypeKey() { + return capitalizeFirstCharacter(this.workItemType).replace(' ', ''); + }, }, methods: { setCommentText(newText) { @@ -158,66 +183,73 @@ export default { <template> <div class="timeline-discussion-body gl-overflow-visible!"> <div class="note-body gl-p-0! gl-overflow-visible!"> - <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1"> - <markdown-editor - :value="commentText" - :render-markdown-path="markdownPreviewPath" - :markdown-docs-path="$options.constantOptions.markdownDocsPath" - :autocomplete-data-sources="autocompleteDataSources" - :form-field-props="formFieldProps" - :add-spacing-classes="false" - data-testid="work-item-add-comment" - class="gl-mb-5" - use-bottom-toolbar - supports-quick-actions - :autofocus="autofocus" - @input="setCommentText" - @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })" - @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })" - @keydown.esc.stop="cancelEditing" - /> - <gl-form-checkbox - v-if="isNewDiscussion" - v-model="isNoteInternal" - class="gl-mb-2" - data-testid="internal-note-checkbox" + <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1 new-note"> + <comment-field-layout + :with-alert-container="isWorkItemConfidential" + :noteable-data="getWorkItemData" + :noteable-type="workItemTypeKey" > - {{ $options.i18n.internal }} - <gl-icon - v-gl-tooltip:tooltipcontainer.bottom - name="question-o" - :size="16" - :title="$options.i18n.internalVisibility" - class="gl-text-blue-500" + <markdown-editor + :value="commentText" + :render-markdown-path="markdownPreviewPath" + :markdown-docs-path="$options.constantOptions.markdownDocsPath" + :autocomplete-data-sources="autocompleteDataSources" + :form-field-props="formFieldProps" + :add-spacing-classes="false" + data-testid="work-item-add-comment" + use-bottom-toolbar + supports-quick-actions + :autofocus="autofocus" + @input="setCommentText" + @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })" + @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })" + @keydown.esc.stop="cancelEditing" + /> + </comment-field-layout> + <div class="note-form-actions"> + <gl-form-checkbox + v-if="isNewDiscussion" + v-model="isNoteInternal" + class="gl-mb-2" + data-testid="internal-note-checkbox" + > + {{ $options.i18n.internal }} + <gl-icon + v-gl-tooltip:tooltipcontainer.bottom + name="question-o" + :size="16" + :title="$options.i18n.internalVisibility" + class="gl-text-blue-500" + /> + </gl-form-checkbox> + <gl-button + category="primary" + variant="confirm" + data-testid="confirm-button" + :disabled="!commentText.length" + :loading="isSubmitting" + @click="$emit('submitForm', { commentText, isNoteInternal })" + >{{ commentButtonTextComputed }} + </gl-button> + <work-item-state-toggle-button + v-if="isNewDiscussion" + class="gl-ml-3" + :work-item-id="workItemId" + :work-item-state="workItemState" + :work-item-type="workItemType" + can-update + @error="$emit('error', $event)" /> - </gl-form-checkbox> - <gl-button - category="primary" - variant="confirm" - data-testid="confirm-button" - :disabled="!commentText.length" - :loading="isSubmitting" - @click="$emit('submitForm', { commentText, isNoteInternal })" - >{{ commentButtonTextComputed }} - </gl-button> - <work-item-state-toggle-button - v-if="isNewDiscussion" - class="gl-ml-3" - :work-item-id="workItemId" - :work-item-state="workItemState" - :work-item-type="workItemType" - can-update - @error="$emit('error', $event)" - /> - <gl-button - v-else - data-testid="cancel-button" - category="primary" - class="gl-ml-3" - :loading="updateInProgress" - @click="cancelEditing" - >{{ $options.i18n.cancelButtonText }} - </gl-button> + <gl-button + v-else + data-testid="cancel-button" + category="primary" + class="gl-ml-3" + :loading="updateInProgress" + @click="cancelEditing" + >{{ $options.i18n.cancelButtonText }} + </gl-button> + </div> </form> </div> </div> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue index f030363664f..fd8842aa01a 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue @@ -65,6 +65,11 @@ export default { required: false, default: false, }, + isWorkItemConfidential: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -235,6 +240,7 @@ export default { :autocomplete-data-sources="autocompleteDataSources" :markdown-preview-path="markdownPreviewPath" :is-internal-thread="note.internal" + :is-work-item-confidential="isWorkItemConfidential" @startReplying="showReplyForm" @cancelEditing="hideReplyForm" @replied="onReplied" diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue index 92560f2da9e..b5e3ea68725 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue @@ -163,6 +163,9 @@ export default { projectName() { return this.workItem?.project?.name; }, + isWorkItemConfidential() { + return this.workItem?.confidential; + }, }, apollo: { workItem: { @@ -314,6 +317,7 @@ export default { :markdown-preview-path="markdownPreviewPath" :work-item-id="workItemId" :autofocus="isEditing" + :is-work-item-confidential="isWorkItemConfidential" class="gl-pl-3 gl-mt-3" @cancelEditing="isEditing = false" @submitForm="updateNote" diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index c12f7374610..8146b66dc1f 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -600,6 +600,7 @@ export default { :assignees="workItemAssignees && workItemAssignees.assignees.nodes" :can-set-work-item-metadata="canAssignUnassignUser" :report-abuse-path="reportAbusePath" + :is-work-item-confidential="workItem.confidential" class="gl-pt-5" @error="updateError = $event" @has-notes="updateHasNotes" diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue index 8fc460294e6..256f8ed53d1 100644 --- a/app/assets/javascripts/work_items/components/work_item_notes.vue +++ b/app/assets/javascripts/work_items/components/work_item_notes.vue @@ -79,6 +79,11 @@ export default { type: String, required: true, }, + isWorkItemConfidential: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -124,6 +129,7 @@ export default { isNewDiscussion: true, markdownPreviewPath: this.markdownPreviewPath, autocompleteDataSources: this.autocompleteDataSources, + isWorkItemConfidential: this.isWorkItemConfidential, }; }, notesArray() { @@ -366,6 +372,7 @@ export default { :markdown-preview-path="markdownPreviewPath" :assignees="assignees" :can-set-work-item-metadata="canSetWorkItemMetadata" + :is-work-item-confidential="isWorkItemConfidential" @deleteNote="showDeleteNoteModal($event, discussion)" @reportAbuse="reportAbuse(true, $event)" @error="$emit('error', $event)" diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index 8140ee97291..6e9379a5926 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -62,7 +62,7 @@ module EnvironmentHelper klass = "ci-status ci-#{status.dasherize} #{ci_icon_utilities}" text = "#{ci_icon_for_status(status)} <span class=\"gl-ml-2\">#{status_text}</span>".html_safe - if deployment.deployable + if deployment.deployable.instance_of?(::Ci::Build) link_to(text, deployment_path(deployment), class: klass) else content_tag(:span, text, class: klass) diff --git a/app/models/user.rb b/app/models/user.rb index 7f84d1e7310..849c3824ae1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2050,7 +2050,7 @@ class User < MainClusterwide::ApplicationRecord end def user_detail - super.presence || build_user_detail + super.presence || (persisted? ? create_user_detail! : build_user_detail) end def pending_todo_for(target) diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb index cc179ba964a..eb09f315953 100644 --- a/app/services/users/update_service.rb +++ b/app/services/users/update_service.rb @@ -21,7 +21,6 @@ module Users yield(@user) if block user_exists = @user.persisted? - @user.user_detail # prevent assignment discard_read_only_attributes assign_attributes diff --git a/config/initializers/00_deprecations.rb b/config/initializers/00_deprecations.rb index 3d6a6491176..120e818e49a 100644 --- a/config/initializers/00_deprecations.rb +++ b/config/initializers/00_deprecations.rb @@ -44,7 +44,8 @@ else # https://gitlab.com/gitlab-org/gitlab/-/issues/414556 /Merging .* no longer maintain both conditions, and will be replaced by the latter in Rails 7\.0/, # https://gitlab.com/gitlab-org/gitlab/-/issues/415890 - /(Date|Time|TimeWithZone)#to_s.+ is deprecated/ + /(Date|Time|TimeWithZone)#to_s.+ is deprecated/, + /Sum of non-numeric elements requires an initial argument/ ] view_component_3_warnings = [ diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index f7876e16464..d879668649d 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -30,7 +30,9 @@ See also [Mass inserting Rails models](mass_insert.md). **LARGE_PROJECTS**: Create large projects (through import) from a predefined set of URLs. -### Seeding issues for all or a given project +### Seeding Data + +#### Seeding issues for all projects or a single project You can seed issues for all or a given project with the `gitlab:seed:issues` task: @@ -197,6 +199,14 @@ bundle exec rake "gitlab:seed:ci_variables_instance" bundle exec rake "gitlab:seed:ci_variables_instance[25, CI_VAR_]" ``` +#### Seed a project for merge train development + +Seeds a project with merge trains configured and 20 merge requests(each with 3 commits). The command: + +```shell +rake gitlab:seed:merge_trains:project +``` + ### Automation If you're very sure that you want to **wipe the current database** and refill diff --git a/doc/update/versions/gitlab_15_changes.md b/doc/update/versions/gitlab_15_changes.md index a70d137f1a9..f343f306490 100644 --- a/doc/update/versions/gitlab_15_changes.md +++ b/doc/update/versions/gitlab_15_changes.md @@ -558,6 +558,13 @@ A [license caching issue](https://gitlab.com/gitlab-org/gitlab/-/issues/376706) upgrading to 15.2.0 or later: 1. Ensure all GitLab web nodes are running GitLab 15.1.Z. + 1. If you run [GitLab on Kubernetes](https://docs.gitlab.com/charts/installation/) by using the cloud native GitLab Helm chart, make sure that all + webservice pods are running GitLab 15.1.Z: + + ```shell + kubectl get pods -l app=webservice -o custom-columns=webservice-image:{.spec.containers[0].image},workhorse-image:{.spec.containers[1].image} + ``` + 1. [Enable the `active_support_hash_digest_sha256` feature flag](../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) to switch `ActiveSupport::Digest` to use SHA256: 1. [Start the rails console](../../administration/operations/rails_console.md) diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md index 944c5eac709..5d5ada0ebb5 100644 --- a/doc/user/application_security/dependency_list/index.md +++ b/doc/user/application_security/dependency_list/index.md @@ -73,6 +73,7 @@ Dependency paths are supported for the following package managers: - [NuGet](https://www.nuget.org/) - [Yarn 1.x](https://classic.yarnpkg.com/lang/en/) - [sbt](https://www.scala-sbt.org) +- [Conan](https://conan.io) ### Licenses diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 663bbe17b5d..6e98869e564 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -41516,7 +41516,7 @@ msgstr "" msgid "ScanResultPolicy|Attributes are automatically applied by the scanners" msgstr "" -msgid "ScanResultPolicy|Block users from unprotecting branches" +msgid "ScanResultPolicy|Block users from modifying protected branches" msgstr "" msgid "ScanResultPolicy|Choose criteria type" @@ -56173,6 +56173,9 @@ msgstr "" msgid "jigsaw is not defined" msgstr "" +msgid "key result" +msgstr "" + msgid "kuromoji custom analyzer" msgstr "" @@ -56744,6 +56747,9 @@ msgstr "" msgid "nounSeries|%{item}, and %{lastItem}" msgstr "" +msgid "objective" +msgstr "" + msgid "on or after" msgstr "" @@ -57087,6 +57093,9 @@ msgstr "" msgid "targeting " msgstr "" +msgid "task" +msgstr "" + msgid "terraform states" msgstr "" diff --git a/qa/lib/gitlab/page/group/settings/billing.rb b/qa/lib/gitlab/page/group/settings/billing.rb index 0565da0d353..086cb42a778 100644 --- a/qa/lib/gitlab/page/group/settings/billing.rb +++ b/qa/lib/gitlab/page/group/settings/billing.rb @@ -17,7 +17,6 @@ module Gitlab # Usage p :seats_in_subscription p :seats_currently_in_use - link :see_seats_usage p :max_seats_used p :seats_owed diff --git a/qa/lib/gitlab/page/group/settings/billing.stub.rb b/qa/lib/gitlab/page/group/settings/billing.stub.rb index c49d744d61f..9aa1a23ec14 100644 --- a/qa/lib/gitlab/page/group/settings/billing.stub.rb +++ b/qa/lib/gitlab/page/group/settings/billing.stub.rb @@ -197,30 +197,6 @@ module Gitlab # This is a stub, used for indexing. The method is dynamically generated. end - # @note Defined as +link :see_seats_usage+ - # Clicks +see_seats_usage+ - def see_seats_usage - # This is a stub, used for indexing. The method is dynamically generated. - end - - # @example - # Gitlab::Page::Group::Settings::Billing.perform do |billing| - # expect(billing.see_seats_usage_element).to exist - # end - # @return [Watir::Link] The raw +Link+ element - def see_seats_usage_element - # This is a stub, used for indexing. The method is dynamically generated. - end - - # @example - # Gitlab::Page::Group::Settings::Billing.perform do |billing| - # expect(billing).to be_see_seats_usage - # end - # @return [Boolean] true if the +see_seats_usage+ element is present on the page - def see_seats_usage? - # This is a stub, used for indexing. The method is dynamically generated. - end - # @note Defined as +p :max_seats_used+ # @return [String] The text content or value of +max_seats_used+ def max_seats_used diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 67c857165fc..e29ed02297f 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -111,6 +111,10 @@ FactoryBot.define do last_sign_in_ip { '127.0.0.1' } end + trait :with_user_detail do + after :build, &:user_detail + end + trait :with_credit_card_validation do after :create do |user| create :credit_card_validation, user: user diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb index 5a803ee2a0d..807a7ca8e26 100644 --- a/spec/finders/deployments_finder_spec.rb +++ b/spec/finders/deployments_finder_spec.rb @@ -280,6 +280,22 @@ RSpec.describe DeploymentsFinder, feature_category: :deployment_management do it { is_expected.to match_array([deployment_2]) } end end + + context 'with mixed deployable types' do + let!(:deployment_1) do + create(:deployment, :success, project: project, deployable: create(:ci_build)) + end + + let!(:deployment_2) do + create(:deployment, :success, project: project, deployable: create(:ci_bridge)) + end + + let(:params) { { **base_params, status: 'success' } } + + it 'successfuly fetches deployments' do + is_expected.to contain_exactly(deployment_1, deployment_2) + end + end end context 'at group scope' do diff --git a/spec/fixtures/api/schemas/job/job.json b/spec/fixtures/api/schemas/job/job.json index f3d5e9b038a..34668f309a6 100644 --- a/spec/fixtures/api/schemas/job/job.json +++ b/spec/fixtures/api/schemas/job/job.json @@ -5,7 +5,6 @@ "id", "name", "started", - "build_path", "playable", "created_at", "updated_at", diff --git a/spec/fixtures/api/schemas/status/ci_detailed_status.json b/spec/fixtures/api/schemas/status/ci_detailed_status.json index 8d0f1e4a6af..0d9e4975858 100644 --- a/spec/fixtures/api/schemas/status/ci_detailed_status.json +++ b/spec/fixtures/api/schemas/status/ci_detailed_status.json @@ -17,7 +17,7 @@ "group": { "type": "string" }, "tooltip": { "type": "string" }, "has_details": { "type": "boolean" }, - "details_path": { "type": "string" }, + "details_path": { "oneOf": [{ "type": "null" }, {"type": "string" }] }, "favicon": { "type": "string" }, "illustration": { "$ref": "illustration.json" }, "action": { "$ref": "action.json" } diff --git a/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js b/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js index 537f8114fcf..57f4911e5f3 100644 --- a/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js +++ b/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js @@ -9,7 +9,7 @@ import { createAlert } from '~/alert'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { organizationGroups } from '../mock_data'; +import { organizationGroups } from '~/organizations/mock_data'; jest.mock('~/alert'); diff --git a/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js b/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js index 7cadcab5021..2965de7cfc8 100644 --- a/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js +++ b/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js @@ -9,7 +9,7 @@ import { createAlert } from '~/alert'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { organizationProjects } from '../mock_data'; +import { organizationProjects } from '~/organizations/mock_data'; jest.mock('~/alert'); diff --git a/spec/frontend/organizations/groups_and_projects/utils_spec.js b/spec/frontend/organizations/groups_and_projects/utils_spec.js index 4469ef1c4d5..8b88e1ff64d 100644 --- a/spec/frontend/organizations/groups_and_projects/utils_spec.js +++ b/spec/frontend/organizations/groups_and_projects/utils_spec.js @@ -1,7 +1,7 @@ import { formatProjects, formatGroups } from '~/organizations/groups_and_projects/utils'; import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { organizationProjects, organizationGroups } from './mock_data'; +import { organizationProjects, organizationGroups } from '~/organizations/mock_data'; describe('formatProjects', () => { it('correctly formats the projects', () => { diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js index 4b1b7b27ad9..826fc2b2230 100644 --- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js @@ -255,6 +255,20 @@ describe('Work item add note', () => { expect(wrapper.emitted('error')).toEqual([[error]]); }); + + it('sends confidential prop to work item comment form', async () => { + await createComponent({ isEditing: true, signedIn: true }); + + const { + data: { + workspace: { + workItems: { nodes }, + }, + }, + } = workItemByIidResponseFactory({ canUpdate: true, canCreateNote: true }); + + expect(findCommentForm().props('isWorkItemConfidential')).toBe(nodes[0].confidential); + }); }); }); diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js index c5d1decfb42..9049a69656a 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js @@ -388,6 +388,13 @@ describe('Work Item Note', () => { }); }); + it('confidential information on note', async () => { + createComponent(); + await findNoteActions().vm.$emit('startEditing'); + const { confidential } = workItemByIidResponseFactory().data.workspace.workItems.nodes[0]; + expect(findCommentForm().props('isWorkItemConfidential')).toBe(confidential); + }); + describe('author and user role badges', () => { describe('author badge props', () => { it.each` diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index d3c7c9e2074..638a92dbd17 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -586,7 +586,10 @@ describe('WorkItemDetail component', () => { createComponent(); await waitForPromises(); + const { confidential } = workItemQueryResponse.data.workspace.workItems.nodes[0]; + expect(findNotesWidget().exists()).toBe(true); + expect(findNotesWidget().props('isWorkItemConfidential')).toBe(confidential); }); }); diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index c2821cc99f9..35f01c85ec8 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -88,6 +88,7 @@ describe('WorkItemNotes component', () => { defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler, deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler, isModal = false, + isWorkItemConfidential = false, } = {}) => { wrapper = shallowMount(WorkItemNotes, { apolloProvider: createMockApollo([ @@ -106,6 +107,7 @@ describe('WorkItemNotes component', () => { workItemType: 'task', reportAbusePath: '/report/abuse/path', isModal, + isWorkItemConfidential, }, stubs: { GlModal: stubComponent(GlModal, { methods: { show: showModal } }), @@ -344,4 +346,14 @@ describe('WorkItemNotes component', () => { }); }); }); + + it('passes confidential props when the work item is confidential', async () => { + createComponent({ + isWorkItemConfidential: true, + defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler, + }); + await waitForPromises(); + + expect(findWorkItemCommentNoteAtIndex(0).props('isWorkItemConfidential')).toBe(true); + }); }); diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index ad81c125055..adae96f34fa 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -203,7 +203,7 @@ RSpec.describe ApplicationHelper do describe '#linkedin_url?' do using RSpec::Parameterized::TableSyntax - let(:user) { build_stubbed(:user) } + let(:user) { build_stubbed(:user, user_detail: build_stubbed(:user_detail)) } subject { helper.linkedin_url(user) } @@ -230,7 +230,7 @@ RSpec.describe ApplicationHelper do describe '#twitter_url?' do using RSpec::Parameterized::TableSyntax - let(:user) { build_stubbed(:user) } + let(:user) { build_stubbed(:user, user_detail: build_stubbed(:user_detail)) } subject { helper.twitter_url(user) } diff --git a/spec/helpers/environment_helper_spec.rb b/spec/helpers/environment_helper_spec.rb index b9316e46d9d..1383bf34881 100644 --- a/spec/helpers/environment_helper_spec.rb +++ b/spec/helpers/environment_helper_spec.rb @@ -22,6 +22,15 @@ RSpec.describe EnvironmentHelper, feature_category: :environment_management do end end + context 'when deploying from a bridge' do + it 'renders a span tag' do + deploy = build(:deployment, deployable: create(:ci_bridge), status: :success) + html = helper.render_deployment_status(deploy) + + expect(html).to have_css('span.ci-status.ci-success') + end + end + context 'for a blocked deployment' do subject { helper.render_deployment_status(deployment) } diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb index 366032100de..e75aa2e2729 100644 --- a/spec/helpers/sessions_helper_spec.rb +++ b/spec/helpers/sessions_helper_spec.rb @@ -54,7 +54,7 @@ RSpec.describe SessionsHelper, feature_category: :system_access do describe '#unconfirmed_verification_email?', :freeze_time do using RSpec::Parameterized::TableSyntax - let(:user) { build_stubbed(:user) } + let(:user) { build_stubbed(:user, user_detail: build_stubbed(:user_detail)) } let(:token_valid_for) { ::Users::EmailVerification::ValidateTokenService::TOKEN_VALID_FOR_MINUTES } subject { helper.unconfirmed_verification_email?(user) } @@ -101,7 +101,7 @@ RSpec.describe SessionsHelper, feature_category: :system_access do end describe '#verification_data' do - let(:user) { build_stubbed(:user) } + let(:user) { build_stubbed(:user, user_detail: build_stubbed(:user_detail)) } it 'returns the expected data' do expect(helper.verification_data(user)).to eq({ diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0ed745f28fd..ded024462b5 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -219,28 +219,53 @@ RSpec.describe User, feature_category: :user_profile do end describe '#user_detail' do - it 'does not persist `user_detail` by default' do - expect(create(:user).user_detail).not_to be_persisted + context 'when user is not persisted' do + let(:user) { build(:user) } + + it 'builds `user_detail`' do + expect(user.user_detail).to be_instance_of(UserDetail) + expect(user.user_detail).not_to be_persisted + end end - shared_examples 'delegated field' do |field| - it 'creates `user_detail` when the field is given' do - user = create(:user, field => 'my field') + context 'when user is persisted' do + let(:user) { create(:user) } + + it 'creates `user_detail`' do + expect(UserDetail.exists?(user_id: user.id)).to eq(false) + + user.user_detail + + expect(UserDetail.exists?(user_id: user.id)).to eq(true) + expect(user.user_detail).to be_instance_of(UserDetail) expect(user.user_detail).to be_persisted - expect(user.user_detail[field]).to eq('my field') end + end - it 'delegates to `user_detail`' do - user = create(:user, field => 'my field') + shared_examples 'delegated field' do |field, value = 'field value'| + context "when #{field} field" do + it 'creates `user_detail` when the field is given', :aggregate_failures do + user = create(:user, field => value) - expect(user.public_send(field)).to eq(user.user_detail[field]) - end + expect(user.user_detail).to be_persisted + expect(user.user_detail[field]).to eq(value) + end - it 'creates `user_detail` when first updated' do - user = create(:user) + it 'delegates the field to `user_detail`' do + user = create(:user, field => value) + + expect(user.public_send(field)).to eq(user.user_detail[field]) + end + + it 'creates `user_detail` when the field is first updated', :aggregate_failures do + user = create(:user) - expect { user.update!(field => 'my field') }.to change { user.user_detail.persisted? }.from(false).to(true) + user.update!(field => value) + + expect(user.user_detail).to be_persisted + expect(user.user_detail[field]).to eq(value) + end end end @@ -250,36 +275,27 @@ RSpec.describe User, feature_category: :user_profile do it_behaves_like 'delegated field', :skype it_behaves_like 'delegated field', :location it_behaves_like 'delegated field', :organization + it_behaves_like 'delegated field', :website_url, 'https://example.com' + it_behaves_like 'delegated field', :pronouns, 'they/them' + it_behaves_like 'delegated field', :pronunciation, 'uhg-zaam-pl' - it 'creates `user_detail` when `website_url` is given' do - user = create(:user, website_url: 'https://example.com') - - expect(user.user_detail).to be_persisted - expect(user.user_detail.website_url).to eq('https://example.com') - end - - it 'delegates `website_url` to `user_detail`' do - user = create(:user, website_url: 'http://example.com') - - expect(user.website_url).to eq(user.user_detail.website_url) - end - - it 'creates `user_detail` when `website_url` is first updated' do - user = create(:user) + context 'when race condition' do + it 'handles it properly' do + user = create(:user) + stale_user = described_class.find(user.id) - expect { user.update!(website_url: 'https://example.com') }.to change { user.user_detail.persisted? }.from(false).to(true) - end + user.user_detail + stale_user.user_detail - it 'delegates `pronouns` to `user_detail`' do - user = create(:user, pronouns: 'they/them') + user.update!(bio: 'hello') - expect(user.pronouns).to eq(user.user_detail.pronouns) - end + expect { stale_user.update!(pronunciation: 'my-pronunciation') }.not_to raise_error - it 'delegates `pronunciation` to `user_detail`' do - user = create(:user, name: 'Example', pronunciation: 'uhg-zaam-pl') + user.reload - expect(user.pronunciation).to eq(user.user_detail.pronunciation) + expect(user.bio).to eq('hello') + expect(user.pronunciation).to eq('my-pronunciation') + end end end @@ -450,7 +466,7 @@ RSpec.describe User, feature_category: :user_profile do describe 'validations' do describe 'password' do - let!(:user) { build_stubbed(:user) } + let!(:user) { build_stubbed(:user, user_detail: build_stubbed(:user_detail)) } before do allow(Devise).to receive(:password_length).and_return(8..128) @@ -622,7 +638,7 @@ RSpec.describe User, feature_category: :user_profile do end context 'when username is changed' do - let(:user) { build_stubbed(:user, username: 'old_path', namespace: build_stubbed(:user_namespace)) } + let(:user) { build_stubbed(:user, username: 'old_path', namespace: build_stubbed(:user_namespace), user_detail: build_stubbed(:user_detail)) } it 'validates move_dir is allowed for the namespace' do expect(user.namespace).to receive(:any_project_has_container_registry_tags?).and_return(true) diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index 4aba83dae92..3a475267dc1 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -8,7 +8,7 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, :public, group: group) } let_it_be(:current_user) { create(:user) } - let_it_be(:reporter) { create(:user).tap { |reporter| project.add_reporter(reporter) } } + let_it_be(:reporter) { create(:user, :with_user_detail).tap { |reporter| project.add_reporter(reporter) } } let_it_be(:label1) { create(:label, project: project) } let_it_be(:label2) { create(:label, project: project) } let_it_be(:milestone1) { create(:milestone, project: project) } @@ -410,7 +410,7 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team # TODO: Fix N+1 queries executed for the linked work item widgets # https://gitlab.com/gitlab-org/gitlab/-/issues/420605 expect { post_graphql(query, current_user: current_user) } - .not_to exceed_all_query_limit(control).with_threshold(11) + .not_to exceed_all_query_limit(control).with_threshold(13) end end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index f3e5f3ab891..025f9887c62 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -67,7 +67,7 @@ RSpec.describe API::Members, feature_category: :groups_and_projects do get api(members_url, maintainer) end - project.add_developer(create(:user)) + project.add_developer(create(:user, :with_user_detail)) expect do get api(members_url, maintainer) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index d3d1a2a6cd0..9140db8f81f 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -206,7 +206,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do end describe "User with no identities" do - let(:user) { create(:user) } + let(:user) { create(:user, :with_user_detail) } context "when the project doesn't exist" do context "when namespace doesn't exist" do diff --git a/spec/requests/search_controller_spec.rb b/spec/requests/search_controller_spec.rb index 365b20ad4aa..7815e22dc46 100644 --- a/spec/requests/search_controller_spec.rb +++ b/spec/requests/search_controller_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe SearchController, type: :request, feature_category: :global_search do - let_it_be(:user) { create(:user) } + let_it_be(:user) { create(:user, :with_user_detail) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :public, :repository, :wiki_repo, name: 'awesome project', group: group) } let_it_be(:projects) { create_list(:project, 5, :public, :repository, :wiki_repo) } @@ -44,7 +44,7 @@ RSpec.describe SearchController, type: :request, feature_category: :global_searc let(:params) { { search: 'foo', scope: 'issues' } } # some N+1 queries still exist # each issue runs an extra query for group namespaces - let(:threshold) { 1 } + let(:threshold) { 3 } it_behaves_like 'an efficient database result' end diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index b0f3f328a4f..53f0e0c34e1 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -10,8 +10,11 @@ RSpec.describe DeploymentEntity do let_it_be(:environment) { create(:environment, project: project) } let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project, user: user) } let_it_be_with_reload(:build) { create(:ci_build, :manual, :environment_with_deployment_tier, pipeline: pipeline) } + let_it_be_with_reload(:bridge) do + create(:ci_bridge, :manual, :environment_with_deployment_tier, pipeline: pipeline, downstream: project) + end - let_it_be_with_refind(:deployment) { create(:deployment, deployable: build, environment: environment) } + let!(:deployment) { create(:deployment, deployable: job, environment: environment, project: project) } let(:request) { double('request') } let(:entity) { described_class.new(deployment, request: request) } @@ -28,22 +31,33 @@ RSpec.describe DeploymentEntity do allow(request).to receive(:project).and_return(project) end - it 'exposes fields', :aggregate_failures do - expect(subject).to include(:iid) - expect(subject[:ref][:name]).to eq 'master' - expect(subject).to include(:status) - expect(subject).to include(:created_at) - expect(subject).to include(:deployed_at) - expect(subject).to include(:is_last) - expect(subject).to include(:tier_in_yaml) + shared_examples_for 'exposes fields' do + it 'exposes fields', :aggregate_failures do + expect(subject).to include(:iid) + expect(subject[:ref][:name]).to eq 'master' + expect(subject).to include(:status) + expect(subject).to include(:created_at) + expect(subject).to include(:deployed_at) + expect(subject).to include(:is_last) + expect(subject).to include(:tier_in_yaml) + end + end + + context 'when deployable is build job' do + let(:job) { build } + + it_behaves_like 'exposes fields' + end + + context 'when deployable is bridge job' do + let(:job) { bridge } + + it_behaves_like 'exposes fields' end context 'when deployable is nil' do let(:entity) { described_class.new(deployment, request: request, deployment_details: false) } - - before do - deployment.update!(deployable: nil) - end + let(:job) { nil } it 'does not expose deployable entry' do expect(subject).not_to include(:deployable) @@ -51,15 +65,17 @@ RSpec.describe DeploymentEntity do end context 'when the pipeline has another manual action' do - let_it_be(:other_build) do - create(:ci_build, :manual, name: 'another deploy', pipeline: pipeline, environment: build.environment) + let!(:other_job) do + create(:ci_build, :manual, name: 'another deploy', pipeline: pipeline, environment: job.environment) end - let_it_be(:other_deployment) { create(:deployment, deployable: build, environment: environment) } + let!(:other_deployment) { create(:deployment, deployable: job, environment: environment) } + + let(:job) { build } it 'returns another manual action' do - expect(subject[:manual_actions].count).to eq(1) - expect(subject[:manual_actions].pluck(:name)).to match_array(['another deploy']) + expect(subject[:manual_actions].count).to eq(2) + expect(subject[:manual_actions].pluck(:name)).to match_array(['another deploy', 'bridge']) end context 'when user is a reporter' do @@ -82,18 +98,22 @@ RSpec.describe DeploymentEntity do end describe 'scheduled_actions' do - let(:build) { create(:ci_build, :success, pipeline: pipeline) } - - before do - deployment.update!(deployable: build) - end + let(:job) { create(:ci_build, :success, pipeline: pipeline) } context 'when the same pipeline has a scheduled action' do - let(:other_build) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other build') } - let!(:other_deployment) { create(:deployment, deployable: other_build, environment: environment) } + let(:other_job) { create(:ci_build, :schedulable, :success, pipeline: pipeline, name: 'other job') } + let!(:other_deployment) { create(:deployment, deployable: other_job, environment: environment) } it 'returns other scheduled actions' do - expect(subject[:scheduled_actions][0][:name]).to eq 'other build' + expect(subject[:scheduled_actions][0][:name]).to eq 'other job' + end + + context 'when deployable is bridge job' do + let(:job) { create(:ci_bridge, :success, pipeline: pipeline) } + + it 'returns nil' do + expect(subject[:scheduled_actions]).to be_nil + end end end @@ -115,10 +135,6 @@ RSpec.describe DeploymentEntity do end describe 'playable_build' do - before do - deployment.update!(deployable: job) - end - context 'when the deployment has a playable deployable' do context 'when this job is build and ready to be played' do let(:job) { create(:ci_build, :playable, :scheduled, pipeline: pipeline) } @@ -161,6 +177,8 @@ RSpec.describe DeploymentEntity do described_class.new(deployment, request: request, deployment_details: false) end + let(:job) { build } + it 'does not serialize deployment details' do expect(subject.with_indifferent_access) .not_to include(:commit, :manual_actions, :scheduled_actions) @@ -172,5 +190,16 @@ RSpec.describe DeploymentEntity do .to eq(name: 'test', build_path: path) end end + + context 'when deployable is bridge' do + let(:job) { bridge } + + it 'only exposes deployable name and path' do + project_job_path(project, deployment.deployable).tap do |path| + expect(subject.fetch(:deployable)) + .to eq(name: 'bridge', build_path: path) + end + end + end end end diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb index 0a93e300eb6..79bf0d972d4 100644 --- a/spec/services/deployments/update_environment_service_spec.rb +++ b/spec/services/deployments/update_environment_service_spec.rb @@ -79,6 +79,27 @@ RSpec.describe Deployments::UpdateEnvironmentService, feature_category: :continu expect(subject.execute).to eq(deployment) end + context 'when deployable is bridge job' do + let(:job) do + create(:ci_bridge, + :with_deployment, + pipeline: pipeline, + ref: 'master', + tag: false, + environment: environment_name, + options: { environment: options }, + project: project) + end + + it 'creates ref' do + expect_any_instance_of(Repository) + .to receive(:create_ref) + .with(deployment.sha, "refs/environments/production/deployments/#{deployment.iid}") + + service.execute + end + end + context 'when start action is defined' do let(:options) { { name: 'production', action: 'start' } } diff --git a/spec/support/shared_examples/ci/deployable_shared_examples.rb b/spec/support/shared_examples/ci/deployable_shared_examples.rb index b51a8fa20e2..6f56d9dae11 100644 --- a/spec/support/shared_examples/ci/deployable_shared_examples.rb +++ b/spec/support/shared_examples/ci/deployable_shared_examples.rb @@ -96,7 +96,7 @@ RSpec.shared_examples 'a deployable job' do ActiveRecord::QueryRecorder.new { subject } end - index_for_build = recorded.log.index { |l| l.include?("UPDATE #{Ci::Build.quoted_table_name}") } + index_for_build = recorded.log.index { |l| l.include?("UPDATE #{described_class.quoted_table_name}") } index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") } expect(index_for_build).to be < index_for_deployment @@ -259,7 +259,7 @@ RSpec.shared_examples 'a deployable job' do describe '#environment_tier_from_options' do subject { job.environment_tier_from_options } - let(:job) { Ci::Build.new(options: options) } + let(:job) { described_class.new(options: options) } let(:options) { { environment: { deployment_tier: 'production' } } } it { is_expected.to eq('production') } @@ -276,7 +276,7 @@ RSpec.shared_examples 'a deployable job' do let(:options) { { environment: { deployment_tier: 'production' } } } let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) } - let(:job) { Ci::Build.new(options: options, environment: 'production', project: project) } + let(:job) { described_class.new(options: options, environment: 'production', project: project) } it { is_expected.to eq('production') } @@ -536,10 +536,6 @@ RSpec.shared_examples 'a deployable job' do end describe '#deployment_status' do - before do - allow_any_instance_of(Ci::Build).to receive(:create_deployment) # rubocop:disable RSpec/AnyInstanceOf - end - context 'when job is a last deployment' do let(:job) { create(factory_type, :success, environment: 'production', pipeline: pipeline) } let(:environment) { create(:environment, name: 'production', project: job.project) } |