diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-04 18:08:35 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-04 18:08:35 +0000 |
commit | e15501a5e1f54249434167c0198dab775bdc4a1f (patch) | |
tree | 15615908225f23633fa269c063de38d38f88c38a | |
parent | 856e2c64ee69b055b31a8ebbeee616f13a46505e (diff) | |
download | gitlab-ce-e15501a5e1f54249434167c0198dab775bdc4a1f.tar.gz |
Add latest changes from gitlab-org/gitlab@master
53 files changed, 516 insertions, 151 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 4767f100cf4..7787c28d318 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -114,14 +114,16 @@ policy: push .qa-ruby-gems-cache: &qa-ruby-gems-cache - key: "qa-ruby-gems-${DEBIAN_VERSION}" + key: + files: + - qa/Gemfile.lock paths: - - qa/vendor/ruby/ + - qa/vendor/ruby policy: pull .qa-ruby-gems-cache-push: &qa-ruby-gems-cache-push <<: *qa-ruby-gems-cache - policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up. + policy: pull-push .setup-test-env-cache: cache: diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 8881a4c486d..93cd43fbdee 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -1,4 +1,5 @@ .qa-job-base: + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:debian-bullseye-ruby-2.7-bundler-2.3-git-2.33-lfs-2.9-chrome-99-docker-20.10.14-gcloud-383-kubectl-1.23 extends: - .default-retry - .qa-cache diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index a185d40664e..ba5e8c9b112 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -5,18 +5,9 @@ include: - /ci/allure-report.yml - /ci/knapsack-report.yml -.review-qa-base: - extends: - - .use-docker-in-docker - image: - name: ${QA_IMAGE} - entrypoint: [""] - stage: qa - needs: ["review-deploy"] +.test_variables: variables: QA_DEBUG: "true" - QA_CAN_TEST_GIT_PROTOCOL_V2: "false" - QA_CAN_TEST_PRAEFECT: "false" QA_GENERATE_ALLURE_REPORT: "true" GITLAB_USERNAME: "root" GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}" @@ -24,19 +15,40 @@ include: GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}" GITLAB_QA_ADMIN_ACCESS_TOKEN: "${REVIEW_APPS_ROOT_TOKEN}" GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}" - SIGNUP_DISABLED: "true" + +.review-qa-base: + extends: + - .use-docker-in-docker + - .qa-cache + - .test_variables + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:debian-bullseye-ruby-2.7-bundler-2.3-git-2.33-lfs-2.9-chrome-99-docker-20.10.14-gcloud-383-kubectl-1.23 + stage: qa + needs: ["review-deploy"] + variables: + DOCKER_HOST: tcp://docker:2376 + DOCKER_TLS_CERTDIR: /certs + DOCKER_CERT_PATH: /certs/client + DOCKER_TLS_VERIFY: 1 + BUNDLE_SUPPRESS_INSTALL_USING_MESSAGES: "true" + BUNDLE_PATH: vendor before_script: - # Use $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA so that GitLab image built in omnibus-gitlab-mirror and QA image are in sync. - export EE_LICENSE="$(cat $REVIEW_APPS_EE_LICENSE_FILE)" - - if [ -n "$CI_MERGE_REQUEST_SOURCE_BRANCH_SHA" ]; then - git checkout -f ${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA}; - fi - - export CI_ENVIRONMENT_URL="$(cat environment_url.txt)" - - echo "${CI_ENVIRONMENT_URL}" - - cd qa + - export QA_GITLAB_URL="$(cat environment_url.txt)" + - cd qa && bundle install script: - qa_run_status=0 - - bin/test "${QA_SCENARIO}" "${CI_ENVIRONMENT_URL}" -- --color --format documentation --format RspecJunitFormatter --out tmp/rspec.xml || qa_run_status=$? + - | + bundle exec rake "knapsack:rspec[\ + ${RSPEC_TAGS} \ + --tag ~orchestrated \ + --tag ~transient \ + --tag ~skip_signup_disabled \ + --tag ~requires_git_protocol_v2 \ + --force-color \ + --order random \ + --format documentation \ + --format RspecJunitFormatter --out tmp/rspec.xml \ + ]" || qa_run_status=$? - if [ ${qa_run_status} -ne 0 ]; then release_sha=$(echo "${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-${CI_COMMIT_SHA}}" | cut -c1-11); echo "Errors can be found at https://sentry.gitlab.net/gitlab/gitlab-review-apps/releases/${release_sha}/all-events/."; @@ -63,21 +75,20 @@ review-qa-smoke: extends: - .review-qa-base - .review:rules:review-qa-smoke - retry: 1 # This is confusing but this means "2 runs at max". + retry: 1 variables: QA_RUN_TYPE: review-qa-smoke - QA_SCENARIO: Test::Instance::Smoke - + RSPEC_TAGS: --tag smoke review-qa-reliable: extends: - .review-qa-base - .review:rules:review-qa-reliable - parallel: 10 retry: 1 + parallel: 10 variables: QA_RUN_TYPE: review-qa-reliable - QA_SCENARIO: Test::Instance::Reliable + RSPEC_TAGS: --tag reliable review-qa-all: extends: @@ -86,8 +97,7 @@ review-qa-all: parallel: 5 variables: QA_RUN_TYPE: review-qa-all - QA_SCENARIO: Test::Instance::All - QA_SKIP_SMOKE_RELIABLE: "true" + RSPEC_TAGS: --tag ~reliable --tag ~smoke review-performance: extends: diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f3b27bdc0..d3adbbe049e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 14.10.2 (2022-05-04) + +### Fixed (2 changes) + +- [Resolve "Fork relationship is not respected for certain projects"](gitlab-org/gitlab@881099bc27d9696ea3b9bcc2a1e43c3207ee4bb3) ([merge request](gitlab-org/gitlab!86476)) +- [Fix mappings errors for ES6.8](gitlab-org/gitlab@5caac54a746a331d828d4e3ce24273cd6173c86f) ([merge request](gitlab-org/gitlab!86476)) **GitLab Enterprise Edition** + +### Other (1 change) + +- [Add documentation for mr settings audit events part 1](gitlab-org/gitlab@95bfdae5a677de5ac9d0d5ceccd42e88ca4f99c4) ([merge request](gitlab-org/gitlab!86476)) + ## 14.10.1 (2022-04-29) ### Security (14 changes) diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js index c743b18d572..c362253f52e 100644 --- a/app/assets/javascripts/api/user_api.js +++ b/app/assets/javascripts/api/user_api.js @@ -12,6 +12,7 @@ const USER_PROJECTS_PATH = '/api/:version/users/:id/projects'; const USER_POST_STATUS_PATH = '/api/:version/user/status'; const USER_FOLLOW_PATH = '/api/:version/users/:id/follow'; const USER_UNFOLLOW_PATH = '/api/:version/users/:id/unfollow'; +const CURRENT_USER_PATH = '/api/:version/user'; export function getUsers(query, options) { const url = buildApiUrl(USERS_PATH); @@ -81,3 +82,8 @@ export function unfollowUser(userId) { const url = buildApiUrl(USER_UNFOLLOW_PATH).replace(':id', encodeURIComponent(userId)); return axios.post(url); } + +export function getCurrentUser(options) { + const url = buildApiUrl(CURRENT_USER_PATH); + return axios.get(url, { ...options }); +} diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index ca04824c663..ce401862cc1 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -25,7 +25,7 @@ import importGroupsMutation from '../graphql/mutations/import_groups.mutation.gr import updateImportStatusMutation from '../graphql/mutations/update_import_status.mutation.graphql'; import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql'; import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql'; -import { NEW_NAME_FIELD, i18n } from '../constants'; +import { NEW_NAME_FIELD, ROOT_NAMESPACE, i18n } from '../constants'; import { StatusPoller } from '../services/status_poller'; import { isFinished, isAvailableForImport, isNameValid, isSameTarget } from '../utils'; import ImportActionsCell from './import_actions_cell.vue'; @@ -430,10 +430,10 @@ export default { return this.importTargets[group.id]; } - const defaultTargetNamespace = this.availableNamespaces[0] ?? { fullPath: '', id: null }; + const defaultTargetNamespace = this.availableNamespaces[0] ?? ROOT_NAMESPACE; let importTarget; if (group.lastImportTarget) { - const targetNamespace = this.availableNamespaces.find( + const targetNamespace = [ROOT_NAMESPACE, ...this.availableNamespaces].find( (ns) => ns.fullPath === group.lastImportTarget.targetNamespace, ); diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue index 344a6e45370..4fbbd5b239c 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue @@ -57,6 +57,7 @@ export default { toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!" class="gl-h-7 gl-flex-grow-1" data-qa-selector="target_namespace_selector_dropdown" + data-testid="target-namespace-selector" > <gl-dropdown-item @click="$emit('update-target-namespace', { fullPath: '', id: null })">{{ s__('BulkImport|No parent') diff --git a/app/assets/javascripts/import_entities/import_groups/constants.js b/app/assets/javascripts/import_entities/import_groups/constants.js index ac1466238d0..32137308684 100644 --- a/app/assets/javascripts/import_entities/import_groups/constants.js +++ b/app/assets/javascripts/import_entities/import_groups/constants.js @@ -18,3 +18,5 @@ export const i18n = { }; export const NEW_NAME_FIELD = 'newName'; + +export const ROOT_NAMESPACE = { fullPath: '', id: null }; diff --git a/app/assets/javascripts/jira_connect/subscriptions/api.js b/app/assets/javascripts/jira_connect/subscriptions/api.js index 14947b6c835..de67703356f 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/api.js +++ b/app/assets/javascripts/jira_connect/subscriptions/api.js @@ -29,3 +29,13 @@ export const fetchGroups = async (groupsPath, { page, perPage, search }) => { }, }); }; + +export const fetchSubscriptions = async (subscriptionsPath) => { + const jwt = await getJwt(); + + return axios.get(subscriptionsPath, { + params: { + jwt, + }, + }); +}; diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue index 1fc40e5c0d6..d77fd5652b2 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue @@ -12,7 +12,7 @@ export default { GroupItemName, }, inject: { - subscriptionsPath: { + addSubscriptionsPath: { default: '', }, }, @@ -36,7 +36,7 @@ export default { onClick() { this.isLoading = true; - addSubscription(this.subscriptionsPath, this.group.full_path) + addSubscription(this.addSubscriptionsPath, this.group.full_path) .then(() => { persistAlert({ title: s__('Integrations|Namespace successfully linked'), diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index 9fdca530aee..f8e0a406c6f 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -1,7 +1,7 @@ <script> import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { isEmpty } from 'lodash'; -import { mapState, mapMutations } from 'vuex'; +import { mapState, mapMutations, mapActions } from 'vuex'; import { retrieveAlert } from '~/jira_connect/subscriptions/utils'; import AccessorUtilities from '~/lib/utils/accessor'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -30,8 +30,8 @@ export default { usersPath: { default: '', }, - subscriptions: { - default: [], + subscriptionsPath: { + default: '', }, }, data() { @@ -40,7 +40,7 @@ export default { }; }, computed: { - ...mapState(['alert']), + ...mapState(['alert', 'subscriptions']), shouldShowAlert() { return Boolean(this.alert?.message); }, @@ -64,16 +64,30 @@ export default { created() { this.setInitialAlert(); }, + mounted() { + this.fetchSubscriptionsOauth(); + }, methods: { ...mapMutations({ setAlert: SET_ALERT, }), + ...mapActions(['fetchSubscriptions']), + /** + * Fetch subscriptions from the REST API, + * if the jiraConnectOauth flag is enabled. + */ + fetchSubscriptionsOauth() { + if (!this.isOauthEnabled) return; + + this.fetchSubscriptions(this.subscriptionsPath); + }, setInitialAlert() { const { linkUrl, title, message, variant } = retrieveAlert() || {}; this.setAlert({ linkUrl, title, message, variant }); }, onSignInOauth(user) { this.user = user; + this.fetchSubscriptionsOauth(); }, onSignInError() { this.setAlert({ diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue index dfed57df7d6..3ee243aad47 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue @@ -8,7 +8,7 @@ import { } from '~/jira_connect/subscriptions/constants'; import { setUrlParams } from '~/lib/utils/url_utility'; import AccessorUtilities from '~/lib/utils/accessor'; - +import { getCurrentUser } from '~/rest_api'; import { createCodeVerifier, createCodeChallenge } from '../pkce'; export default { @@ -40,6 +40,7 @@ export default { // Build the initial OAuth authorization URL const { oauth_authorize_url: oauthAuthorizeURL } = this.oauthMetadata; + const oauthAuthorizeURLWithChallenge = setUrlParams( { code_challenge: codeChallenge, @@ -73,6 +74,7 @@ export default { const code = event.data?.code; try { const accessToken = await this.getOAuthToken(code); + await this.loadUser(accessToken); } catch (e) { this.handleError(); @@ -97,7 +99,7 @@ export default { return data.access_token; }, async loadUser(accessToken) { - const { data } = await axios.get('/api/v4/user', { + const { data } = await getCurrentUser({ headers: { Authorization: `Bearer ${accessToken}` }, }); diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue index 0251728c896..4c039be9ba5 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/subscriptions_list.vue @@ -1,7 +1,7 @@ <script> import { GlButton, GlTableLite } from '@gitlab/ui'; import { isEmpty } from 'lodash'; -import { mapMutations } from 'vuex'; +import { mapMutations, mapState } from 'vuex'; import { removeSubscription } from '~/jira_connect/subscriptions/api'; import { reloadPage } from '~/jira_connect/subscriptions/utils'; import { __, s__ } from '~/locale'; @@ -16,11 +16,6 @@ export default { GroupItemName, TimeagoTooltip, }, - inject: { - subscriptions: { - default: [], - }, - }, data() { return { loadingItem: null, @@ -45,6 +40,9 @@ export default { i18n: { unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'), }, + computed: { + ...mapState(['subscriptions']), + }, methods: { ...mapMutations({ setAlert: SET_ALERT, diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js index d30ebdbb487..df3cf5b1381 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/constants.js +++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js @@ -8,6 +8,9 @@ export const ADD_NAMESPACE_MODAL_ID = 'add-namespace-modal'; export const I18N_DEFAULT_SIGN_IN_BUTTON_TEXT = s__('Integrations|Sign in to GitLab'); export const I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE = s__('Integrations|Failed to sign in to GitLab.'); +export const I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE = s__( + 'Integrations|Failed to load subscriptions.', +); const OAUTH_WINDOW_SIZE = 800; export const OAUTH_WINDOW_OPTIONS = [ diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js index 3b584b5fe98..8e9f73538b9 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/index.js +++ b/app/assets/javascripts/jira_connect/subscriptions/index.js @@ -9,8 +9,6 @@ import JiraConnectApp from './components/app.vue'; import createStore from './store'; import { sizeToParent } from './utils'; -const store = createStore(); - export function initJiraConnect() { const el = document.querySelector('.js-jira-connect-app'); if (!el) { @@ -24,6 +22,7 @@ export function initJiraConnect() { const { groupsPath, subscriptions, + addSubscriptionsPath, subscriptionsPath, usersPath, gitlabUserPath, @@ -31,12 +30,14 @@ export function initJiraConnect() { } = el.dataset; sizeToParent(); + const store = createStore({ subscriptions: JSON.parse(subscriptions) }); + return new Vue({ el, store, provide: { groupsPath, - subscriptions: JSON.parse(subscriptions), + addSubscriptionsPath, subscriptionsPath, usersPath, gitlabUserPath, diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue index a1868e11b53..91b66c87694 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue @@ -40,7 +40,7 @@ export default { <div> <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> <div v-if="hasSubscriptions"> - <div class="gl-display-flex gl-justify-content-end"> + <div class="gl-display-flex gl-justify-content-end gl-mb-3"> <sign-in-oauth-button v-if="useSignInOauthButton" @sign-in="$emit('sign-in-oauth', $event)" diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue index e49c764ebc5..b1c1ae73e14 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions_page.vue @@ -1,5 +1,7 @@ <script> -import { GlEmptyState } from '@gitlab/ui'; +import { mapState } from 'vuex'; +import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; + import SubscriptionsList from '../components/subscriptions_list.vue'; import AddNamespaceButton from '../components/add_namespace_button.vue'; @@ -7,6 +9,7 @@ export default { name: 'SubscriptionsPage', components: { GlEmptyState, + GlLoadingIcon, SubscriptionsList, AddNamespaceButton, }, @@ -16,6 +19,9 @@ export default { required: true, }, }, + computed: { + ...mapState(['subscriptionsLoading', 'subscriptionsError']), + }, }; </script> @@ -23,8 +29,9 @@ export default { <div> <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> - <div v-if="hasSubscriptions"> - <div class="gl-display-flex gl-justify-content-end"> + <gl-loading-icon v-if="subscriptionsLoading" size="md" /> + <div v-else-if="hasSubscriptions && !subscriptionsError"> + <div class="gl-display-flex gl-justify-content-end gl-mb-3"> <add-namespace-button /> </div> diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/actions.js b/app/assets/javascripts/jira_connect/subscriptions/store/actions.js new file mode 100644 index 00000000000..44241535e76 --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/store/actions.js @@ -0,0 +1,22 @@ +import { fetchSubscriptions as fetchSubscriptionsREST } from '~/jira_connect/subscriptions/api'; +import { I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE } from '../constants'; +import { + SET_SUBSCRIPTIONS, + SET_SUBSCRIPTIONS_LOADING, + SET_SUBSCRIPTIONS_ERROR, + SET_ALERT, +} from './mutation_types'; + +export const fetchSubscriptions = async ({ commit }, subscriptionsPath) => { + commit(SET_SUBSCRIPTIONS_LOADING, true); + + try { + const data = await fetchSubscriptionsREST(subscriptionsPath); + commit(SET_SUBSCRIPTIONS, data.data.subscriptions); + } catch { + commit(SET_SUBSCRIPTIONS_ERROR, true); + commit(SET_ALERT, { message: I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE, variant: 'danger' }); + } finally { + commit(SET_SUBSCRIPTIONS_LOADING, false); + } +}; diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/index.js b/app/assets/javascripts/jira_connect/subscriptions/store/index.js index de830e3891a..abad1920bcc 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/store/index.js +++ b/app/assets/javascripts/jira_connect/subscriptions/store/index.js @@ -1,12 +1,15 @@ import Vue from 'vue'; import Vuex from 'vuex'; +import * as actions from './actions'; import mutations from './mutations'; -import state from './state'; +import createState from './state'; Vue.use(Vuex); -export default () => - new Vuex.Store({ +export default function createStore(initialState) { + return new Vuex.Store({ mutations, - state, + actions, + state: createState(initialState), }); +} diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js b/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js index 15f36b824d9..f954c22c906 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js +++ b/app/assets/javascripts/jira_connect/subscriptions/store/mutation_types.js @@ -1 +1,4 @@ export const SET_ALERT = 'SET_ALERT'; +export const SET_SUBSCRIPTIONS = 'SET_SUBSCRIPTIONS'; +export const SET_SUBSCRIPTIONS_LOADING = 'SET_SUBSCRIPTIONS_LOADING'; +export const SET_SUBSCRIPTIONS_ERROR = 'SET_SUBSCRIPTIONS_ERROR'; diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js b/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js index 2a25e0fe25f..d5c4864243b 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js +++ b/app/assets/javascripts/jira_connect/subscriptions/store/mutations.js @@ -1,7 +1,21 @@ -import { SET_ALERT } from './mutation_types'; +import { + SET_ALERT, + SET_SUBSCRIPTIONS, + SET_SUBSCRIPTIONS_LOADING, + SET_SUBSCRIPTIONS_ERROR, +} from './mutation_types'; export default { [SET_ALERT](state, { title, message, variant, linkUrl } = {}) { state.alert = { title, message, variant, linkUrl }; }, + [SET_SUBSCRIPTIONS](state, subscriptions = []) { + state.subscriptions = subscriptions; + }, + [SET_SUBSCRIPTIONS_LOADING](state, subscriptionsLoading) { + state.subscriptionsLoading = subscriptionsLoading; + }, + [SET_SUBSCRIPTIONS_ERROR](state, subscriptionsError) { + state.subscriptionsError = subscriptionsError; + }, }; diff --git a/app/assets/javascripts/jira_connect/subscriptions/store/state.js b/app/assets/javascripts/jira_connect/subscriptions/store/state.js index c807df03f00..2243e70afb1 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/store/state.js +++ b/app/assets/javascripts/jira_connect/subscriptions/store/state.js @@ -1,3 +1,8 @@ -export default () => ({ - alert: undefined, -}); +export default function createState({ subscriptions = [], subscriptionsLoading = false } = {}) { + return { + alert: undefined, + subscriptions, + subscriptionsLoading, + subscriptionsError: false, + }; +} diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue index 51373e712ff..9b0e6560c53 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue @@ -35,7 +35,7 @@ export default { }, computed: { ...mapState(['pageInfo']), - ...mapGetters(['getSuiteTests', 'getSuiteTestCount']), + ...mapGetters(['getSuiteTests', 'getSuiteTestCount', 'getSuiteArtifactsExpired']), hasSuites() { return this.getSuiteTests.length > 0; }, @@ -80,7 +80,8 @@ export default { <div v-for="(testCase, index) in getSuiteTests" :key="index" - class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row" + class="gl-responsive-table-row rounded align-items-md-start" + data-testid="test-case-row" > <div class="table-section section-20 section-wrap"> <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div> @@ -157,7 +158,16 @@ export default { </div> <div v-else> - <p class="js-no-test-cases">{{ s__('TestReports|There are no test cases to display.') }}</p> + <p data-testid="no-test-cases"> + {{ s__('TestReports|There are no test cases to display.') }} + </p> + <p v-if="getSuiteArtifactsExpired" data-testid="artifacts-expired"> + {{ + s__( + 'TestReports|Test details are populated by job artifacts. The job artifacts from this pipeline are expired.', + ) + }} + </p> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js index b7f590a7b3c..f0556f3d12e 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js @@ -38,11 +38,7 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => { return axios .get(state.suiteEndpoint, { params: { build_ids } }) .then(({ data }) => commit(types.SET_SUITE, { suite: data, index })) - .catch(() => { - createFlash({ - message: s__('TestReports|There was an error fetching the test suite.'), - }); - }) + .catch((error) => commit(types.SET_SUITE_ERROR, error)) .finally(() => { dispatch('toggleLoading'); }); diff --git a/app/assets/javascripts/pipelines/stores/test_reports/constants.js b/app/assets/javascripts/pipelines/stores/test_reports/constants.js new file mode 100644 index 00000000000..8eebfb6b208 --- /dev/null +++ b/app/assets/javascripts/pipelines/stores/test_reports/constants.js @@ -0,0 +1 @@ +export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts have expired'; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js index 03680de0fa9..e6a88bb4175 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js @@ -1,4 +1,5 @@ import { addIconStatus, formatFilePath, formattedTime } from './utils'; +import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from './constants'; export const getTestSuites = (state) => { const { test_suites: testSuites = [] } = state.testReports; @@ -29,3 +30,6 @@ export const getSuiteTests = (state) => { }; export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0; + +export const getSuiteArtifactsExpired = (state) => + state.errorMessage === ARTIFACTS_EXPIRED_ERROR_MESSAGE; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js index 803f6bf60b1..7651a2f4327 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js @@ -2,4 +2,5 @@ export const SET_PAGE = 'SET_PAGE'; export const SET_SELECTED_SUITE_INDEX = 'SET_SELECTED_SUITE_INDEX'; export const SET_SUMMARY = 'SET_SUMMARY'; export const SET_SUITE = 'SET_SUITE'; +export const SET_SUITE_ERROR = 'SET_SUITE_ERROR'; export const TOGGLE_LOADING = 'TOGGLE_LOADING'; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js index cf0bf8483dd..68ee063dda7 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js @@ -1,3 +1,5 @@ +import createFlash from '~/flash'; +import { s__ } from '~/locale'; import * as types from './mutation_types'; export default { @@ -13,6 +15,18 @@ export default { state.testReports.test_suites[index] = { ...suite, hasFullSuite: true }; }, + [types.SET_SUITE_ERROR](state, error) { + const errorMessage = error.response?.data?.errors; + + if (errorMessage) { + state.errorMessage = errorMessage; + } else { + createFlash({ + message: s__('TestReports|There was an error fetching the test suite.'), + }); + } + }, + [types.SET_SELECTED_SUITE_INDEX](state, selectedSuiteIndex) { Object.assign(state, { selectedSuiteIndex }); }, diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js index 0ee6f53fa58..3ec9418c14e 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/state.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js @@ -5,6 +5,7 @@ export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => testReports: {}, selectedSuiteIndex: null, isLoading: false, + errorMessage: null, pageInfo: { page: 1, perPage: 20, diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index 67b85b26f9e..30f29e002b8 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -7,7 +7,8 @@ module JiraConnectHelper { groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }), subscriptions: subscriptions.map { |s| serialize_subscription(s) }.to_json, - subscriptions_path: jira_connect_subscriptions_path, + add_subscriptions_path: jira_connect_subscriptions_path, + subscriptions_path: jira_connect_subscriptions_path(format: :json), users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in gitlab_user_path: current_user ? user_path(current_user) : nil, oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data.to_json : nil diff --git a/config/settings.rb b/config/settings.rb index 1860ea0f659..df67fdc8c53 100644 --- a/config/settings.rb +++ b/config/settings.rb @@ -195,9 +195,9 @@ class Settings < Settingslogic # Set a default UUID for the case when the UUID hasn't been initialized. uuid = Gitlab::CurrentSettings.uuid || 'uuid-not-set' - minute = Digest::MD5.hexdigest(uuid + 'minute').to_i(16) % 60 - hour = Digest::MD5.hexdigest(uuid + 'hour').to_i(16) % 24 - day_of_week = Digest::MD5.hexdigest(uuid).to_i(16) % 7 + minute = Digest::SHA256.hexdigest(uuid + 'minute').to_i(16) % 60 + hour = Digest::SHA256.hexdigest(uuid + 'hour').to_i(16) % 24 + day_of_week = Digest::SHA256.hexdigest(uuid).to_i(16) % 7 "#{minute} #{hour} * * #{day_of_week}" end diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md index 6953d859768..a3240a6041b 100644 --- a/doc/administration/operations/fast_ssh_key_lookup.md +++ b/doc/administration/operations/fast_ssh_key_lookup.md @@ -134,11 +134,11 @@ This overview is brief. Refer to the above instructions for more context. ## Use `gitlab-sshd` instead of OpenSSH -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299109) in GitLab 14.5 as an **Alpha** release. -> - [Changed](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/6321) to a **Beta** release in GitLab 15.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299109) in GitLab 14.5 as an **Alpha** release for self-managed customers. -NOTE: -`gitlab-sshd` is in [**Beta**](../../policy/alpha-beta-support.md#beta-features). +WARNING: +`gitlab-sshd` is in [**Alpha**](../../policy/alpha-beta-support.md#alpha-features). +It is not ready for production use. `gitlab-sshd` is [a standalone SSH server](https://gitlab.com/gitlab-org/gitlab-shell/-/tree/main/internal/sshd) written in Go. It is provided as a part of the `gitlab-shell` package. It has a lower memory diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index e8ff7b8b865..9f222f157a1 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -533,6 +533,25 @@ Instead of: Do not use **list** when referring to a [**dropdown list**](#dropdown-list). Use the full phrase **dropdown list** instead. +## license + +When writing about licenses: + +- Do not use variations such as **cloud license**, **offline license**, or **legacy license**. +- Do not use interchangeably with **subscription**: + - A license grants users access to the subscription they purchased, and contains information such as the number of seats and subscription dates. + - A subscription is the subscription tier that the user purchases. + +Use: + + - Add a license to your instance. + - Purchase a subscription. + +Instead of: + + - Buy a license. + - Purchase a license. + ## log in, log on Do not use **log in** or **log on**. Use [sign in](#sign-in) instead. If the user interface has **Log in**, you can use it. diff --git a/doc/integration/mattermost/index.md b/doc/integration/mattermost/index.md index c8e2df1f88f..5ea723abba9 100644 --- a/doc/integration/mattermost/index.md +++ b/doc/integration/mattermost/index.md @@ -95,6 +95,7 @@ mattermost_external_url 'http://mattermost.example.com' gitlab_rails['enable'] = false redis['enable'] = false postgres_exporter['enable'] = false +grafana['enable'] = false ``` Then follow the appropriate steps in the [Authorize GitLab Mattermost section](#authorize-gitlab-mattermost). Last, to enable diff --git a/lib/gitlab/experimentation/controller_concern.rb b/lib/gitlab/experimentation/controller_concern.rb index a68e2db4dac..b09d67b8d5f 100644 --- a/lib/gitlab/experimentation/controller_concern.rb +++ b/lib/gitlab/experimentation/controller_concern.rb @@ -146,9 +146,9 @@ module Gitlab return experimentation_subject_id if subject.blank? if subject.respond_to?(:to_global_id) - Digest::MD5.hexdigest(subject.to_global_id.to_s) + Digest::SHA256.hexdigest(subject.to_global_id.to_s) else - Digest::MD5.hexdigest(subject.to_s) + Digest::SHA256.hexdigest(subject.to_s) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a361fb733a2..36ca4f8f69f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -20705,6 +20705,9 @@ msgstr "" msgid "Integrations|Failed to load namespaces. Please try again." msgstr "" +msgid "Integrations|Failed to load subscriptions." +msgstr "" + msgid "Integrations|Failed to sign in to GitLab." msgstr "" @@ -37746,6 +37749,9 @@ msgstr "" msgid "TestReports|No test cases were found in the test report." msgstr "" +msgid "TestReports|Test details are populated by job artifacts. The job artifacts from this pipeline are expired." +msgstr "" + msgid "TestReports|Tests" msgstr "" diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index e3d139d59ae..0a92553690f 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -78,7 +78,7 @@ module QA end def fabricate! - return fabricate_large_merge_request if Runtime::Scenario.large_setup? + return fabricate_large_merge_request if large_setup? populate_target_and_source_if_required @@ -100,7 +100,7 @@ module QA end def fabricate_via_api! - return fabricate_large_merge_request if Runtime::Scenario.large_setup? + return fabricate_large_merge_request if large_setup? resource_web_url(api_get) rescue ResourceNotFoundError, NoValueError # rescue if iid not populated @@ -208,6 +208,12 @@ module QA private + def large_setup? + Runtime::Scenario.large_setup? + rescue ArgumentError + false + end + def transform_api_resource(api_resource) raise ResourceNotFoundError if api_resource.blank? diff --git a/qa/qa/support/knapsack_report.rb b/qa/qa/support/knapsack_report.rb index 0ec53461e3f..afe60664bec 100644 --- a/qa/qa/support/knapsack_report.rb +++ b/qa/qa/support/knapsack_report.rb @@ -100,6 +100,7 @@ module QA # @return [void] def setup_environment! ENV["KNAPSACK_TEST_FILE_PATTERN"] ||= "qa/specs/features/**/*_spec.rb" + ENV["KNAPSACK_TEST_DIR"] = "qa/specs" ENV["KNAPSACK_REPORT_PATH"] = report_path end diff --git a/qa/tasks/knapsack.rake b/qa/tasks/knapsack.rake index c3a2deab7f3..f50716e03bc 100644 --- a/qa/tasks/knapsack.rake +++ b/qa/tasks/knapsack.rake @@ -3,7 +3,12 @@ namespace :knapsack do desc "Run tests with knapsack runner" task :rspec, [:rspec_args] do |_, args| - raise "This environment is not compatible with knapsack runner!" unless QA::Runtime::Env.knapsack? + unless QA::Runtime::Env.knapsack? + QA::Runtime::Logger.info("This environment is not compatible with parallel knapsack execution!") + QA::Runtime::Logger.info("Falling back to standard execution") + + exec(%Q[bundle exec rspec #{args[:rspec_args]}]) + end QA::Support::KnapsackReport.configure! Knapsack::Runners::RSpecRunner.run(args[:rspec_args]) diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 0c2465678f9..1de0e7e6c26 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -118,7 +118,7 @@ RSpec.describe Settings do allow(Gitlab::CurrentSettings) .to receive(:uuid) { 'd9e2f4e8-db1f-4e51-b03d-f427e1965c4a'} - expect(described_class.send(:cron_for_service_ping)).to eq('21 18 * * 4') + expect(described_class.send(:cron_for_service_ping)).to eq('44 10 * * 4') end it 'returns min, hour, day in the valid range' do diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index 3bef793f2c3..1939e43e5dc 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -9,7 +9,7 @@ import createFlash from '~/flash'; import httpStatus from '~/lib/utils/http_status'; import axios from '~/lib/utils/axios_utils'; import { STATUSES } from '~/import_entities/constants'; -import { i18n } from '~/import_entities/import_groups/constants'; +import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants'; import ImportTable from '~/import_entities/import_groups/components/import_table.vue'; import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; @@ -45,6 +45,8 @@ describe('import table', () => { const findImportButtons = () => wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import'); const findPaginationDropdown = () => wrapper.find('[data-testid="page-size"]'); + const findTargetNamespaceDropdown = (rowWrapper) => + rowWrapper.find('[data-testid="target-namespace-selector"]'); const findPaginationDropdownText = () => findPaginationDropdown().find('button').text(); const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]'); @@ -137,6 +139,32 @@ describe('import table', () => { expect(wrapper.findAll('tbody tr')).toHaveLength(FAKE_GROUPS.length); }); + it('correctly maintains root namespace as last import target', async () => { + createComponent({ + bulkImportSourceGroups: () => ({ + nodes: [ + { + ...generateFakeEntry({ id: 1, status: STATUSES.FINISHED }), + lastImportTarget: { + id: 1, + targetNamespace: ROOT_NAMESPACE.fullPath, + newName: 'does-not-matter', + }, + }, + ], + pageInfo: FAKE_PAGE_INFO, + versionValidation: FAKE_VERSION_VALIDATION, + }), + }); + + await waitForPromises(); + const firstRow = wrapper.find('tbody tr'); + const targetNamespaceDropdownButton = findTargetNamespaceDropdown(firstRow).find( + '[aria-haspopup]', + ); + expect(targetNamespaceDropdownButton.text()).toBe('No parent'); + }); + it('does not render status string when result list is empty', async () => { createComponent({ bulkImportSourceGroups: jest.fn().mockResolvedValue({ diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js index 3d7bf7acb41..3cdb21dbe95 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js @@ -13,7 +13,7 @@ jest.mock('~/jira_connect/subscriptions/utils'); describe('GroupsListItem', () => { let wrapper; - const mockSubscriptionPath = 'subscriptionPath'; + const mockAddSubscriptionsPath = '/addSubscriptionsPath'; const createComponent = ({ mountFn = shallowMount } = {}) => { wrapper = mountFn(GroupsListItem, { @@ -21,7 +21,7 @@ describe('GroupsListItem', () => { group: mockGroup1, }, provide: { - subscriptionsPath: mockSubscriptionPath, + addSubscriptionsPath: mockAddSubscriptionsPath, }, }); }; @@ -70,7 +70,10 @@ describe('GroupsListItem', () => { await waitForPromises(); - expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path); + expect(addSubscriptionSpy).toHaveBeenCalledWith( + mockAddSubscriptionsPath, + mockGroup1.full_path, + ); expect(persistAlert).toHaveBeenCalledWith({ linkUrl: '/help/integration/jira_development_panel.html#use-the-integration', message: diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index f9dcf5ade24..a70c2f6c5f7 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -12,6 +12,7 @@ import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; import { I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants'; import { __ } from '~/locale'; import AccessorUtilities from '~/lib/utils/accessor'; +import * as api from '~/jira_connect/subscriptions/api'; import { mockSubscription } from '../mock_data'; jest.mock('~/jira_connect/subscriptions/utils', () => ({ @@ -31,7 +32,8 @@ describe('JiraConnectApp', () => { const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert); const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => { - store = createStore(); + store = createStore({ subscriptions: [mockSubscription] }); + jest.spyOn(store, 'dispatch').mockImplementation(); wrapper = mountFn(JiraConnectApp, { store, @@ -53,7 +55,6 @@ describe('JiraConnectApp', () => { createComponent({ provide: { usersPath, - subscriptions: [mockSubscription], }, }); }); @@ -79,14 +80,13 @@ describe('JiraConnectApp', () => { createComponent({ provide: { usersPath: '/user', - subscriptions: [], }, }); const userLink = findUserLink(); expect(userLink.exists()).toBe(true); expect(userLink.props()).toEqual({ - hasSubscriptions: false, + hasSubscriptions: true, user: null, userSignedIn: false, }); @@ -167,7 +167,6 @@ describe('JiraConnectApp', () => { createComponent({ provide: { usersPath: '/mock', - subscriptions: [], }, }); findSignInPage().vm.$emit('sign-in-oauth', mockUser); @@ -193,7 +192,6 @@ describe('JiraConnectApp', () => { createComponent({ provide: { usersPath: '/mock', - subscriptions: [], }, }); findSignInPage().vm.$emit('error'); @@ -235,4 +233,31 @@ describe('JiraConnectApp', () => { }); }, ); + + describe('when `jiraConnectOauth` feature flag is enabled', () => { + const mockSubscriptionsPath = '/mockSubscriptionsPath'; + + beforeEach(() => { + jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } }); + + createComponent({ + provide: { + glFeatures: { jiraConnectOauth: true }, + subscriptionsPath: mockSubscriptionsPath, + }, + }); + }); + + describe('when component mounts', () => { + it('dispatches `fetchSubscriptions` action', async () => { + expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath); + }); + }); + + describe('when oauth button emits `sign-in-oauth` event', () => { + it('dispatches `fetchSubscriptions` action', () => { + expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath); + }); + }); + }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js index 18274cd4362..1002c48afe0 100644 --- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js @@ -11,9 +11,12 @@ import axios from '~/lib/utils/axios_utils'; import waitForPromises from 'helpers/wait_for_promises'; import httpStatus from '~/lib/utils/http_status'; import AccessorUtilities from '~/lib/utils/accessor'; +import { getCurrentUser } from '~/rest_api'; jest.mock('~/lib/utils/accessor'); jest.mock('~/jira_connect/subscriptions/utils'); +jest.mock('~/jira_connect/subscriptions/api'); +jest.mock('~/rest_api'); jest.mock('~/jira_connect/subscriptions/pkce', () => ({ createCodeVerifier: jest.fn().mockReturnValue('mock-verifier'), createCodeChallenge: jest.fn().mockResolvedValue('mock-challenge'), @@ -147,7 +150,7 @@ describe('SignInOauthButton', () => { mockAxios .onPost(mockOauthMetadata.oauth_token_url) .replyOnce(httpStatus.OK, { access_token: mockAccessToken }); - mockAxios.onGet('/api/v4/user').replyOnce(httpStatus.OK, mockUser); + getCurrentUser.mockResolvedValue({ data: mockUser }); window.dispatchEvent(new MessageEvent('message', mockEvent)); @@ -162,7 +165,7 @@ describe('SignInOauthButton', () => { }); it('executes GET request to fetch user data', () => { - expect(axios.get).toHaveBeenCalledWith('/api/v4/user', { + expect(getCurrentUser).toHaveBeenCalledWith({ headers: { Authorization: `Bearer ${mockAccessToken}` }, }); }); diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js index 2aad533f677..2d7c58fc278 100644 --- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js @@ -20,12 +20,11 @@ describe('SubscriptionsList', () => { let store; const createComponent = () => { - store = createStore(); + store = createStore({ + subscriptions: [mockSubscription], + }); wrapper = mount(SubscriptionsList, { - provide: { - subscriptions: [mockSubscription], - }, store, }); }; diff --git a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js index 31a58042a05..4956af76ead 100644 --- a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js +++ b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js @@ -1,4 +1,4 @@ -import { GlEmptyState } from '@gitlab/ui'; +import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions_page.vue'; import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue'; @@ -10,15 +10,16 @@ describe('SubscriptionsPage', () => { let store; const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton); + const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList); const findEmptyState = () => wrapper.findComponent(GlEmptyState); - const createComponent = ({ props } = {}) => { - store = createStore(); + const createComponent = ({ props, initialState } = {}) => { + store = createStore(initialState); wrapper = shallowMount(SubscriptionsPage, { store, - propsData: props, + propsData: { hasSubscriptions: false, ...props }, stubs: { GlEmptyState, }, @@ -31,29 +32,40 @@ describe('SubscriptionsPage', () => { describe('template', () => { describe.each` - scenario | expectSubscriptionsList | expectEmptyState - ${'with subscriptions'} | ${true} | ${false} - ${'without subscriptions'} | ${false} | ${true} - `('$scenario', ({ expectEmptyState, expectSubscriptionsList }) => { - beforeEach(() => { - createComponent({ - props: { - hasSubscriptions: expectSubscriptionsList, - }, + scenario | subscriptionsLoading | hasSubscriptions | expectSubscriptionsList | expectEmptyState + ${'with subscriptions loading'} | ${true} | ${false} | ${false} | ${false} + ${'with subscriptions'} | ${false} | ${true} | ${true} | ${false} + ${'without subscriptions'} | ${false} | ${false} | ${false} | ${true} + `( + '$scenario', + ({ subscriptionsLoading, hasSubscriptions, expectEmptyState, expectSubscriptionsList }) => { + beforeEach(() => { + createComponent({ + initialState: { subscriptionsLoading }, + props: { + hasSubscriptions, + }, + }); }); - }); - it('renders button to add namespace', () => { - expect(findAddNamespaceButton().exists()).toBe(true); - }); + it(`${ + subscriptionsLoading ? 'does not render' : 'renders' + } button to add namespace`, () => { + expect(findAddNamespaceButton().exists()).toBe(!subscriptionsLoading); + }); - it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => { - expect(findEmptyState().exists()).toBe(expectEmptyState); - }); + it(`${subscriptionsLoading ? 'renders' : 'does not render'} GlLoadingIcon`, () => { + expect(findGlLoadingIcon().exists()).toBe(subscriptionsLoading); + }); - it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => { - expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList); - }); - }); + it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => { + expect(findEmptyState().exists()).toBe(expectEmptyState); + }); + + it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => { + expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList); + }); + }, + ); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js new file mode 100644 index 00000000000..fbc814155b0 --- /dev/null +++ b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js @@ -0,0 +1,63 @@ +import testAction from 'helpers/vuex_action_helper'; + +import * as types from '~/jira_connect/subscriptions/store/mutation_types'; +import { fetchSubscriptions } from '~/jira_connect/subscriptions/store/actions'; +import state from '~/jira_connect/subscriptions/store/state'; +import * as api from '~/jira_connect/subscriptions/api'; +import { I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants'; + +describe('JiraConnect actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('fetchSubscriptions', () => { + const mockUrl = '/mock-url'; + + describe('when API request is successful', () => { + it('should commit SET_SUBSCRIPTIONS_LOADING and SET_SUBSCRIPTIONS mutations', async () => { + jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } }); + + await testAction( + fetchSubscriptions, + mockUrl, + mockedState, + [ + { type: types.SET_SUBSCRIPTIONS_LOADING, payload: true }, + { type: types.SET_SUBSCRIPTIONS, payload: [] }, + { type: types.SET_SUBSCRIPTIONS_LOADING, payload: false }, + ], + [], + ); + + expect(api.fetchSubscriptions).toHaveBeenCalledWith(mockUrl); + }); + }); + + describe('when API request fails', () => { + it('should commit SET_SUBSCRIPTIONS_LOADING, SET_SUBSCRIPTIONS_ERROR and SET_ALERT mutations', async () => { + jest.spyOn(api, 'fetchSubscriptions').mockRejectedValue(); + + await testAction( + fetchSubscriptions, + mockUrl, + mockedState, + [ + { type: types.SET_SUBSCRIPTIONS_LOADING, payload: true }, + { type: types.SET_SUBSCRIPTIONS_ERROR, payload: true }, + { + type: types.SET_ALERT, + payload: { message: I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE, variant: 'danger' }, + }, + { type: types.SET_SUBSCRIPTIONS_LOADING, payload: false }, + ], + [], + ); + + expect(api.fetchSubscriptions).toHaveBeenCalledWith(mockUrl); + }); + }); + }); +}); diff --git a/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js b/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js index 84a33dbf0b5..b5069a9b98d 100644 --- a/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js +++ b/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js @@ -25,4 +25,21 @@ describe('JiraConnect store mutations', () => { }); }); }); + + describe('SET_SUBSCRIPTIONS_LOADING', () => { + it('sets subscriptions loading flag', () => { + mutations.SET_SUBSCRIPTIONS_LOADING(localState, true); + + expect(localState.subscriptionsLoading).toBe(true); + }); + }); + + describe('SET_SUBSCRIPTIONS', () => { + it('sets subscriptions loading flag', () => { + const mockSubscriptions = [{ name: 'test' }]; + mutations.SET_SUBSCRIPTIONS(localState, mockSubscriptions); + + expect(localState.subscriptions).toBe(mockSubscriptions); + }); + }); }); diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js index d5acb115bc1..74a9d8c354f 100644 --- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js @@ -82,17 +82,16 @@ describe('Actions TestReports Store', () => { ); }); - it('should create flash on API error', async () => { + it('should call SET_SUITE_ERROR on error', () => { const index = 0; - await testAction( + return testAction( actions.fetchTestSuite, index, { ...state, testReports, suiteEndpoint: null }, - [], + [{ type: types.SET_SUITE_ERROR, payload: expect.any(Error) }], [{ type: 'toggleLoading' }, { type: 'toggleLoading' }], ); - expect(createFlash).toHaveBeenCalled(); }); describe('when we already have the suite data', () => { diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js index f2dbeec6a06..6ab479a257c 100644 --- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js +++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js @@ -1,6 +1,9 @@ import testReports from 'test_fixtures/pipelines/test_report.json'; import * as types from '~/pipelines/stores/test_reports/mutation_types'; import mutations from '~/pipelines/stores/test_reports/mutations'; +import createFlash from '~/flash'; + +jest.mock('~/flash.js'); describe('Mutations TestReports Store', () => { let mockState; @@ -44,6 +47,24 @@ describe('Mutations TestReports Store', () => { }); }); + describe('set suite error', () => { + it('should set the error message in state if provided', () => { + const message = 'Test report artifacts have expired'; + + mutations[types.SET_SUITE_ERROR](mockState, { + response: { data: { errors: message } }, + }); + + expect(mockState.errorMessage).toBe(message); + }); + + it('should show a flash message otherwise', () => { + mutations[types.SET_SUITE_ERROR](mockState, {}); + + expect(createFlash).toHaveBeenCalled(); + }); + }); + describe('set selected suite index', () => { it('should set selectedSuiteIndex', () => { const selectedSuiteIndex = 0; diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js index 97241e14129..dc72fa31ace 100644 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js @@ -1,12 +1,13 @@ import { GlButton, GlFriendlyWrap, GlLink, GlPagination } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; import testReports from 'test_fixtures/pipelines/test_report.json'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue'; import { TestStatus } from '~/pipelines/constants'; import * as getters from '~/pipelines/stores/test_reports/getters'; import { formatFilePath } from '~/pipelines/stores/test_reports/utils'; +import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from '~/pipelines/stores/test_reports/constants'; import skippedTestCases from './mock_data'; Vue.use(Vuex); @@ -23,13 +24,14 @@ describe('Test reports suite table', () => { const testCases = testSuite.test_cases; const blobPath = '/test/blob/path'; - const noCasesMessage = () => wrapper.find('.js-no-test-cases'); - const allCaseRows = () => wrapper.findAll('.js-case-row'); - const findCaseRowAtIndex = (index) => wrapper.findAll('.js-case-row').at(index); + const noCasesMessage = () => wrapper.findByTestId('no-test-cases'); + const artifactsExpiredMessage = () => wrapper.findByTestId('artifacts-expired'); + const allCaseRows = () => wrapper.findAllByTestId('test-case-row'); + const findCaseRowAtIndex = (index) => wrapper.findAllByTestId('test-case-row').at(index); const findLinkForRow = (row) => row.find(GlLink); const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`); - const createComponent = (suite = testSuite, perPage = 20) => { + const createComponent = ({ suite = testSuite, perPage = 20, errorMessage } = {}) => { store = new Vuex.Store({ state: { blobPath, @@ -41,11 +43,12 @@ describe('Test reports suite table', () => { page: 1, perPage, }, + errorMessage, }, getters, }); - wrapper = shallowMount(SuiteTable, { + wrapper = shallowMountExtended(SuiteTable, { store, stubs: { GlFriendlyWrap }, }); @@ -55,12 +58,18 @@ describe('Test reports suite table', () => { wrapper.destroy(); }); - describe('should not render', () => { - beforeEach(() => createComponent([])); + it('should render a message when there are no test cases', () => { + createComponent({ suite: [] }); - it('a table when there are no test cases', () => { - expect(noCasesMessage().exists()).toBe(true); - }); + expect(noCasesMessage().exists()).toBe(true); + expect(artifactsExpiredMessage().exists()).toBe(false); + }); + + it('should render a message when artifacts have expired', () => { + createComponent({ suite: [], errorMessage: ARTIFACTS_EXPIRED_ERROR_MESSAGE }); + + expect(noCasesMessage().exists()).toBe(true); + expect(artifactsExpiredMessage().exists()).toBe(true); }); describe('when a test suite is supplied', () => { @@ -102,7 +111,7 @@ describe('Test reports suite table', () => { const perPage = 2; beforeEach(() => { - createComponent(testSuite, perPage); + createComponent({ testSuite, perPage }); }); it('renders one page of test cases', () => { @@ -117,11 +126,13 @@ describe('Test reports suite table', () => { describe('when a test case classname property is null', () => { it('still renders all test cases', () => { createComponent({ - ...testSuite, - test_cases: testSuite.test_cases.map((testCase) => ({ - ...testCase, - classname: null, - })), + testSuite: { + ...testSuite, + test_cases: testSuite.test_cases.map((testCase) => ({ + ...testCase, + classname: null, + })), + }, }); expect(allCaseRows()).toHaveLength(testCases.length); @@ -131,11 +142,13 @@ describe('Test reports suite table', () => { describe('when a test case name property is null', () => { it('still renders all test cases', () => { createComponent({ - ...testSuite, - test_cases: testSuite.test_cases.map((testCase) => ({ - ...testCase, - name: null, - })), + testSuite: { + ...testSuite, + test_cases: testSuite.test_cases.map((testCase) => ({ + ...testCase, + name: null, + })), + }, }); expect(allCaseRows()).toHaveLength(testCases.length); diff --git a/spec/helpers/jira_connect_helper_spec.rb b/spec/helpers/jira_connect_helper_spec.rb index 1c1b2a22b7c..169a5c0076a 100644 --- a/spec/helpers/jira_connect_helper_spec.rb +++ b/spec/helpers/jira_connect_helper_spec.rb @@ -23,6 +23,7 @@ RSpec.describe JiraConnectHelper do it 'includes Jira Connect app attributes' do is_expected.to include( :groups_path, + :add_subscriptions_path, :subscriptions_path, :users_path, :subscriptions, diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb index 435a0d56301..799884d7a74 100644 --- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb +++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb @@ -274,7 +274,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do action: 'start', property: 'control_group', value: 1, - label: Digest::MD5.hexdigest('abc'), + label: Digest::SHA256.hexdigest('abc'), user: user ) end @@ -289,7 +289,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do action: 'start', property: 'control_group', value: 1, - label: Digest::MD5.hexdigest('somestring'), + label: Digest::SHA256.hexdigest('somestring'), user: user ) end |