From b3cd77e90438a6c6e837dc27627d4c76f85ecd29 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 4 May 2020 21:09:41 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../static_site_editor/components/app.vue | 3 + .../components/static_site_editor.vue | 96 -------- app/assets/javascripts/static_site_editor/index.js | 11 +- .../javascripts/static_site_editor/pages/home.vue | 96 ++++++++ .../static_site_editor/router/constants.js | 2 + .../javascripts/static_site_editor/router/index.js | 15 ++ .../static_site_editor/router/routes.js | 10 + .../vue_shared/components/clone_dropdown.vue | 14 +- app/models/concerns/has_user_type.rb | 5 +- app/models/user.rb | 10 + app/policies/global_policy.rb | 6 +- app/views/profiles/keys/_form.html.haml | 2 +- .../projects/static_site_editor/show.html.haml | 5 +- changelogs/unreleased/213678-API-documentation.yml | 5 + .../216035-clone-dropdown-icons-alignment.yml | 5 + changelogs/unreleased/fj-add-migration-user.yml | 5 + doc/.vale/gitlab/spelling-exceptions.txt | 2 + doc/api/releases/index.md | 11 +- doc/user/clusters/applications.md | 2 +- doc/user/project/clusters/runbooks/index.md | 166 +++++++------- doc/user/project/integrations/prometheus.md | 8 + doc/user/project/operations/feature_flags.md | 55 ++++- qa/Gemfile | 3 +- qa/Gemfile.lock | 51 +++-- qa/qa/page/profile/ssh_keys.rb | 18 +- qa/qa/resource/ssh_key.rb | 10 +- qa/qa/runtime/api/client.rb | 2 - .../features/api/1_manage/rate_limits_spec.rb | 2 + qa/qa/specs/features/api/1_manage/users_spec.rb | 2 + .../closes_issue_via_pushing_a_commit_spec.rb | 2 + .../features/api/3_create/repository/files_spec.rb | 1 + .../repository/project_archive_compare_spec.rb | 1 + .../email/trigger_email_notification_spec.rb | 2 + .../3_create/repository/add_ssh_key_spec.rb | 2 +- .../repository/push_over_http_file_size_spec.rb | 10 +- qa/qa/support/api.rb | 2 + qa/spec/resource/ssh_key_spec.rb | 21 ++ qa/spec/specs/helpers/quarantine_spec.rb | 8 - spec/factories/users.rb | 4 + .../pipelines/pipeline_details_mediator_spec.js | 36 +++ .../components/static_site_editor_spec.js | 247 --------------------- .../frontend/static_site_editor/pages/home_spec.js | 247 +++++++++++++++++++++ .../__snapshots__/clone_dropdown_spec.js.snap | 22 +- .../pipelines/pipeline_details_mediator_spec.js | 36 --- spec/models/concerns/has_user_type_spec.rb | 4 +- spec/models/user_spec.rb | 16 ++ spec/policies/global_policy_spec.rb | 31 +++ spec/views/admin/users/_user.html.haml_spec.rb | 10 + 48 files changed, 770 insertions(+), 554 deletions(-) create mode 100644 app/assets/javascripts/static_site_editor/components/app.vue delete mode 100644 app/assets/javascripts/static_site_editor/components/static_site_editor.vue create mode 100644 app/assets/javascripts/static_site_editor/pages/home.vue create mode 100644 app/assets/javascripts/static_site_editor/router/constants.js create mode 100644 app/assets/javascripts/static_site_editor/router/index.js create mode 100644 app/assets/javascripts/static_site_editor/router/routes.js create mode 100644 changelogs/unreleased/213678-API-documentation.yml create mode 100644 changelogs/unreleased/216035-clone-dropdown-icons-alignment.yml create mode 100644 changelogs/unreleased/fj-add-migration-user.yml create mode 100644 qa/spec/resource/ssh_key_spec.rb create mode 100644 spec/frontend/pipelines/pipeline_details_mediator_spec.js delete mode 100644 spec/frontend/static_site_editor/components/static_site_editor_spec.js create mode 100644 spec/frontend/static_site_editor/pages/home_spec.js delete mode 100644 spec/javascripts/pipelines/pipeline_details_mediator_spec.js diff --git a/app/assets/javascripts/static_site_editor/components/app.vue b/app/assets/javascripts/static_site_editor/components/app.vue new file mode 100644 index 00000000000..98240aef810 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/components/app.vue @@ -0,0 +1,3 @@ + diff --git a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue deleted file mode 100644 index 79e4b4a4581..00000000000 --- a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue +++ /dev/null @@ -1,96 +0,0 @@ - - diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js index fe5d11f1bd9..b53d461f5a9 100644 --- a/app/assets/javascripts/static_site_editor/index.js +++ b/app/assets/javascripts/static_site_editor/index.js @@ -1,10 +1,11 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; -import StaticSiteEditor from './components/static_site_editor.vue'; +import App from './components/app.vue'; import createStore from './store'; +import createRouter from './router'; const initStaticSiteEditor = el => { - const { isSupportedContent, projectId, path: sourcePath, returnUrl } = el.dataset; + const { isSupportedContent, projectId, path: sourcePath, returnUrl, baseUrl } = el.dataset; const store = createStore({ initialState: { @@ -15,15 +16,17 @@ const initStaticSiteEditor = el => { username: window.gon.current_username, }, }); + const router = createRouter(baseUrl); return new Vue({ el, store, + router, components: { - StaticSiteEditor, + App, }, render(createElement) { - return createElement('static-site-editor', StaticSiteEditor); + return createElement('app'); }, }); }; diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue new file mode 100644 index 00000000000..3de4a4a27cf --- /dev/null +++ b/app/assets/javascripts/static_site_editor/pages/home.vue @@ -0,0 +1,96 @@ + + diff --git a/app/assets/javascripts/static_site_editor/router/constants.js b/app/assets/javascripts/static_site_editor/router/constants.js new file mode 100644 index 00000000000..b835d0b8867 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/router/constants.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const HOME_ROUTE_NAME = 'home'; diff --git a/app/assets/javascripts/static_site_editor/router/index.js b/app/assets/javascripts/static_site_editor/router/index.js new file mode 100644 index 00000000000..12692612bbc --- /dev/null +++ b/app/assets/javascripts/static_site_editor/router/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import routes from './routes'; + +Vue.use(VueRouter); + +export default function createRouter(base) { + const router = new VueRouter({ + base, + mode: 'history', + routes, + }); + + return router; +} diff --git a/app/assets/javascripts/static_site_editor/router/routes.js b/app/assets/javascripts/static_site_editor/router/routes.js new file mode 100644 index 00000000000..72d944fb36d --- /dev/null +++ b/app/assets/javascripts/static_site_editor/router/routes.js @@ -0,0 +1,10 @@ +import Home from '../pages/home.vue'; +import { HOME_ROUTE_NAME } from './constants'; + +export default [ + { + name: HOME_ROUTE_NAME, + path: '/', + component: Home, + }, +]; diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue index 7826c179889..8ebcebb9758 100644 --- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue @@ -4,7 +4,6 @@ import { GlNewDropdownHeader, GlFormInputGroup, GlButton, - GlIcon, GlTooltipDirective, } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; @@ -16,7 +15,6 @@ export default { GlNewDropdownHeader, GlFormInputGroup, GlButton, - GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,9 +57,9 @@ export default { v-gl-tooltip.hover :title="$options.copyURLTooltip" :data-clipboard-text="sshLink" - > - - + icon="copy-to-clipboard" + class="d-inline-flex" + /> @@ -77,9 +75,9 @@ export default { v-gl-tooltip.hover :title="$options.copyURLTooltip" :data-clipboard-text="httpLink" - > - - + icon="copy-to-clipboard" + class="d-inline-flex" + /> diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb index 9b78233e6c6..1347e0be637 100644 --- a/app/models/concerns/has_user_type.rb +++ b/app/models/concerns/has_user_type.rb @@ -10,10 +10,11 @@ module HasUserType visual_review_bot: 3, service_user: 4, ghost: 5, - project_bot: 6 + project_bot: 6, + migration_bot: 7 }.with_indifferent_access.freeze - BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot].freeze + BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot].freeze NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze diff --git a/app/models/user.rb b/app/models/user.rb index 69ca42071fb..942bcf75a88 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -636,6 +636,16 @@ class User < ApplicationRecord end end + def migration_bot + email_pattern = "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}" + + unique_internal(where(user_type: :migration_bot), 'migration-bot', email_pattern) do |u| + u.bio = 'The GitLab migration bot' + u.name = 'GitLab Migration Bot' + u.confirmed_at = Time.zone.now + end + end + # Return true if there is only single non-internal user in the deployment, # ghost user is ignored. def single_user? diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index b6cf945bf5a..03f5a863421 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -18,6 +18,7 @@ class GlobalPolicy < BasePolicy condition(:private_instance_statistics, score: 0) { Gitlab::CurrentSettings.instance_statistics_visibility_private? } condition(:project_bot, scope: :user) { @user&.project_bot? } + condition(:migration_bot, scope: :user) { @user&.migration_bot? } rule { admin | (~private_instance_statistics & ~anonymous) } .enable :read_instance_statistics @@ -48,11 +49,14 @@ class GlobalPolicy < BasePolicy rule { blocked | internal }.policy do prevent :log_in prevent :access_api - prevent :access_git prevent :receive_notifications prevent :use_slash_commands end + rule { blocked | (internal & ~migration_bot) }.policy do + prevent :access_git + end + rule { project_bot }.policy do prevent :log_in prevent :receive_notifications diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index 34e81285328..7709aa8f4b9 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -14,7 +14,7 @@ .col.form-group = f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold' - = f.date_field :expires_at, class: "form-control input-lg qa-key-expiry-field", min: Date.tomorrow + = f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' } .js-add-ssh-key-validation-warning.hide .bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' } diff --git a/app/views/projects/static_site_editor/show.html.haml b/app/views/projects/static_site_editor/show.html.haml index 8d2649be588..88c5378fc35 100644 --- a/app/views/projects/static_site_editor/show.html.haml +++ b/app/views/projects/static_site_editor/show.html.haml @@ -1 +1,4 @@ -#static-site-editor{ data: @config.payload } +-# TODO: Remove after base URL is included in the model !30735 +- data = @config.payload.merge({ base_url: namespace_project_show_sse_path }) + +#static-site-editor{ data: data } diff --git a/changelogs/unreleased/213678-API-documentation.yml b/changelogs/unreleased/213678-API-documentation.yml new file mode 100644 index 00000000000..2fb155b70db --- /dev/null +++ b/changelogs/unreleased/213678-API-documentation.yml @@ -0,0 +1,5 @@ +--- +title: Remove deprecated Release Evidence endpoints documentation +merge_request: 30978 +author: +type: removed diff --git a/changelogs/unreleased/216035-clone-dropdown-icons-alignment.yml b/changelogs/unreleased/216035-clone-dropdown-icons-alignment.yml new file mode 100644 index 00000000000..ab7f1ab2df2 --- /dev/null +++ b/changelogs/unreleased/216035-clone-dropdown-icons-alignment.yml @@ -0,0 +1,5 @@ +--- +title: Fixed alignment of Snippet Clone copy buttons +merge_request: 30897 +author: +type: fixed diff --git a/changelogs/unreleased/fj-add-migration-user.yml b/changelogs/unreleased/fj-add-migration-user.yml new file mode 100644 index 00000000000..ac851add77d --- /dev/null +++ b/changelogs/unreleased/fj-add-migration-user.yml @@ -0,0 +1,5 @@ +--- +title: Add migration bot user +merge_request: 30738 +author: +type: added diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index 995cbf9ab62..bbbb0ae0dad 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -201,6 +201,7 @@ namespaced namespaces Nanoc NGINX +Nurtch OAuth Okta offboarded @@ -277,6 +278,7 @@ resync reverified reverifies reverify +Rubix runbook runbooks runit diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md index d327a76b683..2e1e206ecfd 100644 --- a/doc/api/releases/index.md +++ b/doc/api/releases/index.md @@ -286,7 +286,6 @@ Example response: ], "commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a", "tag_path":"/root/awesome-app/-/tags/v0.11.1", - "evidence_sha":"760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d", "assets":{ "count":5, "sources":[ @@ -314,9 +313,15 @@ Example response: "url":"https://gitlab.example.com/root/awesome-app/-/tags/v0.11.1/binaries/linux-amd64", "external":true } - ], - "evidence_url":"https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json" + ] }, + "evidences":[ + { + sha: "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d", + filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json", + collected_at: "2019-07-16T14:00:12.256Z" + } + ] } ``` diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 69d057ad673..40f75008219 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -366,7 +366,7 @@ will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](h More information on creating executable runbooks can be found in [our Runbooks -documentation](../project/clusters/runbooks/index.md#executable-runbooks). Note that +documentation](../project/clusters/runbooks/index.md#configure-an-executable-runbook-with-gitlab). Note that Ingress must be installed and have an IP address assigned before JupyterHub can be installed. diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md index 5575cd1d2d4..623833af894 100644 --- a/doc/user/project/clusters/runbooks/index.md +++ b/doc/user/project/clusters/runbooks/index.md @@ -4,11 +4,10 @@ Runbooks are a collection of documented procedures that explain how to carry out a particular process, be it starting, stopping, debugging, or troubleshooting a particular system. -Using [Jupyter Notebooks](https://jupyter.org/) and the [Rubix library](https://github.com/Nurtch/rubix), +Using [Jupyter Notebooks](https://jupyter.org/) and the +[Rubix library](https://github.com/Nurtch/rubix), users can get started writing their own executable runbooks. -## Overview - Historically, runbooks took the form of a decision tree or a detailed step-by-step guide depending on the condition or system. @@ -22,121 +21,128 @@ pre-written code blocks or database queries against a given environment. The JupyterHub app offered via GitLab’s Kubernetes integration now ships with Nurtch’s Rubix library, providing a simple way to create DevOps -runbooks. A sample runbook is provided, showcasing common operations. While Rubix makes it -simple to create common Kubernetes and AWS workflows, you can also create them manually without -Rubix. +runbooks. A sample runbook is provided, showcasing common operations. While +Rubix makes it simple to create common Kubernetes and AWS workflows, you can +also create them manually without Rubix. -** + Watch this [video](https://www.youtube.com/watch?v=Q_OqHIIUPjE) -for an overview of how this is accomplished in GitLab!** +for an overview of how this is accomplished in GitLab! ## Requirements To create an executable runbook, you will need: -1. **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the applications. - The simplest way to get started is to add a cluster using one of [GitLab's integrations](../add_remove_clusters.md#create-new-cluster). -1. **Helm Tiller** - Helm is a package manager for Kubernetes and is required to install - all the other applications. It is installed in its own pod inside the cluster which - can run the Helm CLI in a safe environment. -1. **Ingress** - Ingress can provide load balancing, SSL termination, and name-based - virtual hosting. It acts as a web proxy for your applications. -1. **JupyterHub** - [JupyterHub](https://jupyterhub.readthedocs.io/) is a multi-user service for managing notebooks across - a team. Jupyter Notebooks provide a web-based interactive programming environment - used for data analysis, visualization, and machine learning. +- **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the + applications. The simplest way to get started is to add a cluster using one + of [GitLab's integrations](../add_remove_clusters.md#create-new-cluster). +- **Helm Tiller** - Helm is a package manager for Kubernetes and is required to + install all the other applications. It's installed in its own pod inside the + cluster which can run the Helm CLI in a safe environment. +- **Ingress** - Ingress can provide load balancing, SSL termination, and name-based + virtual hosting. It acts as a web proxy for your applications. +- **JupyterHub** - [JupyterHub](https://jupyterhub.readthedocs.io/) is a multi-user + service for managing notebooks across a team. Jupyter Notebooks provide a + web-based interactive programming environment used for data analysis, + visualization, and machine learning. ## Nurtch -Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). Rubix is -an open-source Python library that makes it easy to perform common DevOps tasks inside Jupyter Notebooks. -Tasks such as plotting Cloudwatch metrics and rolling your ECS/Kubernetes app are simplified -down to a couple of lines of code. See the [Nurtch Documentation](http://docs.nurtch.com/en/latest/) -for more information. +Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). +Rubix is an open-source Python library that makes it easy to perform common +DevOps tasks inside Jupyter Notebooks. Tasks such as plotting Cloudwatch metrics +and rolling your ECS/Kubernetes app are simplified down to a couple of lines of +code. See the [Nurtch Documentation](http://docs.nurtch.com/en/latest/) for more +information. ## Configure an executable runbook with GitLab Follow this step-by-step guide to configure an executable runbook in GitLab using -the components outlined above and the preloaded demo runbook. - -### 1. Add a Kubernetes cluster - -Follow the steps outlined in [Create new cluster](../add_remove_clusters.md#create-new-cluster) -to add a Kubernetes cluster to your project. - -### 2. Install Helm Tiller, Ingress, and JupyterHub - -Once the cluster has been provisioned in GKE, click the **Install** button next to the **Helm Tiller** app. - -![install helm](img/helm-install.png) +the components outlined above and the pre-loaded demo runbook. -Once Tiller has been installed successfully, click the **Install** button next to the **Ingress** app. +1. Add a Kubernetes cluster to your project by following the steps outlined in + [Create new cluster](../add_remove_clusters.md#create-new-cluster). +1. After the cluster has been provisioned in GKE, click the **Install** button + next to the **Helm Tiller** application to install Helm Tiller. -![install ingress](img/ingress-install.png) + ![install helm](img/helm-install.png) -Once Ingress has been installed successfully, click the **Install** button next to the **JupyterHub** app. +1. After Helm Tiller has been installed successfully, click the **Install** button next + to the **Ingress** application. -![install jupyterhub](img/jupyterhub-install.png) + ![install ingress](img/ingress-install.png) -### 3. Login to JupyterHub and start the server +1. After Ingress has been installed successfully, click the **Install** button next + to the **JupyterHub** application. You will need the **Jupyter Hostname** provided + here in the next step. -Once JupyterHub has been installed successfully, navigate to the displayed **Jupyter Hostname** URL and click -**Sign in with GitLab**. Authentication is automatically enabled for any user of the GitLab instance via OAuth2. This -will redirect to GitLab in order to authorize JupyterHub to use your GitLab account. Click **Authorize**. + ![install JupyterHub](img/jupyterhub-install.png) -![authorize jupyter](img/authorize-jupyter.png) +1. After JupyterHub has been installed successfully, open the **Jupyter Hostname** + in your browser. Click the **Sign in with GitLab** button to log in to + JupyterHub and start the server. Authentication is enabled for any user of the + GitLab instance with OAuth2. This button redirects you to a page at GitLab + requesting authorization for JupyterHub to use your GitLab account. -Once the application has been authorized you will taken back to the JupyterHub application. Click **Start My Server**. -The server will take a couple of seconds to start. + ![authorize Jupyter](img/authorize-jupyter.png) -### 4. Configure access +1. Click **Authorize**, and you will be redirected to the JupyterHub application. +1. Click **Start My Server**, and the server will start in a few seconds. +1. To configure the runbook's access to your GitLab project, you must enter your + [GitLab Access Token](../../../profile/personal_access_tokens.md) + and your Project ID in the **Setup** section of the demo runbook: -In order for the runbook to access your GitLab project, you will need to enter a -[GitLab Access Token](../../../profile/personal_access_tokens.md) -as well as your Project ID in the **Setup** section of the demo runbook. + 1. Double-click the **DevOps-Runbook-Demo** folder located on the left panel. -Double-click the **DevOps-Runbook-Demo** folder located on the left panel. + ![demo runbook](img/demo-runbook.png) -![demo runbook](img/demo-runbook.png) + 1. Double-click the `Nurtch-DevOps-Demo.ipynb` runbook. -Double-click the "Nurtch-DevOps-Demo.ipynb" runbook. + ![sample runbook](img/sample-runbook.png) -![sample runbook](img/sample-runbook.png) + Jupyter displays the runbook's contents in the right-hand side of the screen. + The **Setup** section displays your `PRIVATE_TOKEN` and your `PROJECT_ID`. + Enter these values, maintaining the single quotes as follows: -The contents on the runbook will be displayed on the right side of the screen. Under the "Setup" section, you will find -entries for both your `PRIVATE_TOKEN` and your `PROJECT_ID`. Enter both these values, conserving the single quotes as follows: + ```sql + PRIVATE_TOKEN = 'n671WNGecHugsdEDPsyo' + PROJECT_ID = '1234567' + ``` -```sql -PRIVATE_TOKEN = 'n671WNGecHugsdEDPsyo' -PROJECT_ID = '1234567' -``` + 1. Update the `VARIABLE_NAME` on the last line of this section to match the name of + the variable you're using for your access token. In this example, our variable + name is `PRIVATE_TOKEN`. -Update the `VARIABLE_NAME` on the last line of this section to match the name of the variable you are using for your -access token. In this example our variable name is `PRIVATE_TOKEN`. + ```sql + VARIABLE_VALUE = project.variables.get('PRIVATE_TOKEN').value + ``` -```sql -VARIABLE_VALUE = project.variables.get('PRIVATE_TOKEN').value -``` +1. To configure the operation of a runbook, create and configure variables: -### 5. Configure an operation + NOTE: **Note:** + For this example, we are using the **Run SQL queries in Notebook** section in the + sample runbook to query a PostgreSQL database. The first four lines of the following + code block define the variables that are required for this query to function: -For this example we'll use the "**Run SQL queries in Notebook**" section in the sample runbook to query -a PostgreSQL database. The first 4 lines of the section define the variables that are required for this query to function. + ```sql + %env DB_USER={project.variables.get('DB_USER').value} + %env DB_PASSWORD={project.variables.get('DB_PASSWORD').value} + %env DB_ENDPOINT={project.variables.get('DB_ENDPOINT').value} + %env DB_NAME={project.variables.get('DB_NAME').value} + ``` -```sql -%env DB_USER={project.variables.get('DB_USER').value} -%env DB_PASSWORD={project.variables.get('DB_PASSWORD').value} -%env DB_ENDPOINT={project.variables.get('DB_ENDPOINT').value} -%env DB_NAME={project.variables.get('DB_NAME').value} -``` + 1. Navigate to **{settings}** **Settings >> CI/CD >> Variables** to create + the variables in your project. -Create the matching variables in your project's **Settings >> CI/CD >> Variables** + ![GitLab variables](img/gitlab-variables.png) -![gitlab variables](img/gitlab-variables.png) + 1. Click **Save variables**. -Back in Jupyter, click the "Run SQL queries in Notebook" heading and the click *Run*. The results will be -displayed in-line as follows: + 1. In Jupyter, click the **Run SQL queries in Notebook** heading, and then click + **Run**. The results are displayed inline as follows: -![PostgreSQL query](img/postgres-query.png) + ![PostgreSQL query](img/postgres-query.png) -You can try other operations such as running shell scripts or interacting with a Kubernetes cluster. Visit the +You can try other operations, such as running shell scripts or interacting with a +Kubernetes cluster. Visit the [Nurtch Documentation](http://docs.nurtch.com/) for more information. diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 0e18b678133..041a70067d4 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -931,6 +931,14 @@ Prerequisites for embedding from a Grafana instance: 1. In GitLab, paste the URL into a Markdown field and save. The chart will take a few moments to render. ![GitLab Rendered Grafana Panel](img/rendered_grafana_embed_v12_5.png) +## Metrics dashboard visibility + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201924) in GitLab 13.0. + +You can set the visibility of the metrics dashboard to **Only Project Members** +or **Everyone With Access**. When set to **Everyone with Access**, the metrics +dashboard, and all of the custom dashboard, YAML files, are visible to authenticated and non-authenticated users. + ## Troubleshooting When troubleshooting issues with a managed Prometheus app, it is often useful to diff --git a/doc/user/project/operations/feature_flags.md b/doc/user/project/operations/feature_flags.md index 1000eecd692..fb6b808c47c 100644 --- a/doc/user/project/operations/feature_flags.md +++ b/doc/user/project/operations/feature_flags.md @@ -43,10 +43,10 @@ To add a new feature flag: 1. Click on the **New Feature Flag** button. 1. Give it a name. - NOTE: **Note:** - A name can contain only lowercase letters, digits, underscores (`_`) - and dashes (`-`), must start with a letter, and cannot end with a dash (`-`) - or an underscore (`_`). + NOTE: **Note:** + A name can contain only lowercase letters, digits, underscores (`_`) + and dashes (`-`), must start with a letter, and cannot end with a dash (`-`) + or an underscore (`_`). 1. Give it a description (optional, 255 characters max). 1. Define environment [specs](#define-environment-specs). If you want the flag on by default, enable the catch-all [wildcard spec (`*`)](#define-environment-specs) @@ -91,6 +91,41 @@ NOTE: **NOTE** We'd highly recommend you to use the [Environment](../../../ci/environments.md) feature in order to quickly assess which flag is enabled per environment. +## Feature flag behavior change in 13.0 + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35555) in GitLab 13.0. + +Starting in GitLab 13.0, you can apply a feature flag strategy across multiple environment specs, +without defining the strategy multiple times. + +This feature is under development and not ready for production use. It is +deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) +can enable it for your instance. + +To enable it: + +```ruby +Feature.enable(:feature_flags_new_version) +``` + +To disable it: + +```ruby +Feature.disable(:feature_flags_new_version) +``` + +### Applying a strategy to environments + +After a strategy is defined, it applies to **All Environments** by default. To +make a strategy apply to a specific environment spec: + +1. Click the **Add Environment** button. +1. Create a new + [spec](../../../ci/environments.md#scoping-environments-with-specs). + +To apply the strategy to multiple environment specs, repeat these steps. + ## Feature Flag strategies GitLab Feature Flag adopts [Unleash](https://unleash.github.io) @@ -155,12 +190,12 @@ To get the access credentials that your application will need to talk to GitLab: 1. Navigate to your project's **Operations > Feature Flags**. 1. Click on the **Configure** button to see the values: - - **API URL**: URL where the client (application) connects to get a list of feature flags. - - **Instance ID**: Unique token that authorizes the retrieval of the feature flags. - - **Application name**: The name of the running environment. For instance, - if the application runs for production server, application name would be - `production` or similar. This value is used for - [the environment spec evaluation](#define-environment-specs). + - **API URL**: URL where the client (application) connects to get a list of feature flags. + - **Instance ID**: Unique token that authorizes the retrieval of the feature flags. + - **Application name**: The name of the running environment. For instance, + if the application runs for a production server, application name would be + `production` or similar. This value is used for + [the environment spec evaluation](#define-environment-specs). NOTE: **Note:** The meaning of these fields might change over time. For example, we are not sure diff --git a/qa/Gemfile b/qa/Gemfile index 458baffcd40..b11d5b1b682 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -7,7 +7,8 @@ gem 'capybara-screenshot', '~> 1.0.23' gem 'rake', '~> 12.3.0' gem 'rspec', '~> 3.7' gem 'selenium-webdriver', '~> 3.12' -gem 'airborne', '~> 0.2.13' +gem 'airborne', '~> 0.3.4' +gem 'rest-client', '~> 2.1.0' gem 'nokogiri', '~> 1.10.9' gem 'rspec-retry', '~> 0.6.1' gem 'rspec_junit_formatter', '~> 0.4.1' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 26c903599c9..6b996ca00f5 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -9,12 +9,12 @@ GEM zeitwerk (~> 2.2) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) - airborne (0.2.13) + airborne (0.3.4) activesupport rack - rack-test (~> 0.6, >= 0.6.2) - rest-client (>= 1.7.3, < 3.0) - rspec (~> 3.1) + rack-test (>= 1.1.0, < 2.0) + rest-client (>= 2.0.2, < 3.0) + rspec (~> 3.8) byebug (9.1.0) capybara (3.29.0) addressable @@ -34,11 +34,12 @@ GEM debase-ruby_core_source (>= 0.10.2) debase-ruby_core_source (0.10.6) diff-lcs (1.3) - domain_name (0.5.20170404) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) faker (1.9.3) i18n (>= 0.7) gitlab-qa (4.0.0) + http-accept (1.7.0) http-cookie (1.0.3) domain_name (~> 0.5) i18n (1.8.2) @@ -48,9 +49,9 @@ GEM launchy (2.4.3) addressable (~> 2.3) method_source (0.9.0) - mime-types (3.1) + mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mime-types-data (3.2020.0425) mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.0) @@ -67,30 +68,31 @@ GEM byebug (~> 9.1) pry (~> 0.10) public_suffix (4.0.1) - rack (2.0.7) - rack-test (0.8.3) + rack (2.2.2) + rack-test (1.1.0) rack (>= 1.0, < 3) rake (12.3.0) regexp_parser (1.6.0) - rest-client (2.0.2) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rspec (3.7.0) - rspec-core (~> 3.7.0) - rspec-expectations (~> 3.7.0) - rspec-mocks (~> 3.7.0) - rspec-core (3.7.1) - rspec-support (~> 3.7.0) - rspec-expectations (3.7.0) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-mocks (3.7.0) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) + rspec-support (~> 3.9.0) rspec-retry (0.6.1) rspec-core (> 3.3) - rspec-support (3.7.0) + rspec-support (3.9.3) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) ruby-debug-ide (0.7.2) @@ -101,11 +103,11 @@ GEM rubyzip (>= 1.2.2) thread_safe (0.3.6) timecop (0.9.1) - tzinfo (1.2.6) + tzinfo (1.2.7) thread_safe (~> 0.1) unf (0.1.4) unf_ext - unf_ext (0.0.7.4) + unf_ext (0.0.7.7) xpath (3.2.0) nokogiri (~> 1.8) zeitwerk (2.3.0) @@ -115,7 +117,7 @@ PLATFORMS DEPENDENCIES activesupport (~> 6.0.2.2) - airborne (~> 0.2.13) + airborne (~> 0.3.4) capybara (~> 3.29.0) capybara-screenshot (~> 1.0.23) debase (~> 0.2.4.1) @@ -126,6 +128,7 @@ DEPENDENCIES parallel_tests (~> 2.29) pry-byebug (~> 3.5.1) rake (~> 12.3.0) + rest-client (~> 2.1.0) rspec (~> 3.7) rspec-retry (~> 0.6.1) rspec_junit_formatter (~> 0.4.1) diff --git a/qa/qa/page/profile/ssh_keys.rb b/qa/qa/page/profile/ssh_keys.rb index 082202f91ca..810877e21ad 100644 --- a/qa/qa/page/profile/ssh_keys.rb +++ b/qa/qa/page/profile/ssh_keys.rb @@ -5,6 +5,7 @@ module QA module Profile class SSHKeys < Page::Base view 'app/views/profiles/keys/_form.html.haml' do + element :key_expiry_date_field element :key_title_field element :key_public_key_field element :add_key_button @@ -19,17 +20,26 @@ module QA end def add_key(public_key, title) - fill_element :key_public_key_field, public_key - fill_element :key_title_field, title + fill_element(:key_public_key_field, public_key) + fill_element(:key_title_field, title) + # Expire in 2 days just in case the key is created just before midnight + fill_expiry_date(Date.today + 2) - click_element :add_key_button + click_element(:add_key_button) + end + + def fill_expiry_date(date) + date = date.strftime('%m/%d/%Y') if date.is_a?(Date) + Date.strptime(date, '%m/%d/%Y') rescue ArgumentError raise "Expiry date must be in mm/dd/yyyy format" + + fill_element(:key_expiry_date_field, date) end def remove_key(title) click_link(title) accept_alert do - click_element :delete_key_button + click_element(:delete_key_button) end end diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb index 3e130aef9e4..b948bf3969b 100644 --- a/qa/qa/resource/ssh_key.rb +++ b/qa/qa/resource/ssh_key.rb @@ -5,12 +5,16 @@ module QA class SSHKey < Base extend Forwardable - attr_accessor :title + attr_reader :title attribute :id def_delegators :key, :private_key, :public_key, :md5_fingerprint + def initialize + self.title = Time.now.to_f + end + def key @key ||= Runtime::Key::RSA.new end @@ -28,6 +32,10 @@ module QA api_post end + def title=(title) + @title = "E2E test key: #{title}" + end + def api_delete QA::Runtime::Logger.debug("Deleting SSH key with title '#{title}' and fingerprint '#{md5_fingerprint}'") diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index b9a3c9184aa..d29571df981 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'airborne' - module QA module Runtime module API diff --git a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb index 819739ac535..1bf435014af 100644 --- a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb +++ b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'airborne' + module QA context 'Manage with IP rate limits', :requires_admin do describe 'Users API' do diff --git a/qa/qa/specs/features/api/1_manage/users_spec.rb b/qa/qa/specs/features/api/1_manage/users_spec.rb index ba1ba204d24..fbc26e81b69 100644 --- a/qa/qa/specs/features/api/1_manage/users_spec.rb +++ b/qa/qa/specs/features/api/1_manage/users_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'airborne' + module QA context 'Manage' do describe 'Users API' do diff --git a/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb b/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb index f14fcc5afce..58d716f759e 100644 --- a/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb +++ b/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'airborne' + module QA context 'Plan' do include Support::Api diff --git a/qa/qa/specs/features/api/3_create/repository/files_spec.rb b/qa/qa/specs/features/api/3_create/repository/files_spec.rb index dc471128dae..92858ba4107 100644 --- a/qa/qa/specs/features/api/3_create/repository/files_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/files_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'airborne' require 'securerandom' module QA diff --git a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb index 5ba434a7781..3ad56e21ad4 100644 --- a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'airborne' require 'securerandom' require 'digest' diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb index 5f7a6981f23..0a577aa07f8 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb @@ -3,6 +3,8 @@ module QA context 'Plan', :orchestrated, :smtp do describe 'Email Notification' do + include Support::Api + let(:user) do Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb index 25866e12185..68bbc1719fc 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -12,7 +12,7 @@ module QA resource.title = key_title end - expect(page).to have_content("Title: #{key_title}") + expect(page).to have_content(key.title) expect(page).to have_content(key.md5_fingerprint) Page::Main::Menu.perform(&:click_settings_link) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb index 9bc4dcbca2a..9b504ad76b4 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb @@ -3,6 +3,8 @@ module QA context 'Create', :requires_admin do describe 'push after setting the file size limit via admin/application_settings' do + include Support::Api + before(:context) do @project = Resource::Project.fabricate_via_api! do |p| p.name = 'project-test-push-limit' @@ -39,12 +41,10 @@ module QA def set_file_size_limit(limit) request = Runtime::API::Request.new(@api_client, '/application/settings') - put request.url, receive_max_input_size: limit + response = put request.url, receive_max_input_size: limit - expect_status(200) - expect(json_body).to match( - a_hash_including(receive_max_input_size: limit) - ) + expect(response.code).to eq(200) + expect(parse_body(response)[:receive_max_input_size]).to eq(limit) end def push_new_file(file_name, wait_for_push: true) diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index d39b4353191..f5e4d4e294b 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'rest-client' + module QA module Support module Api diff --git a/qa/spec/resource/ssh_key_spec.rb b/qa/spec/resource/ssh_key_spec.rb new file mode 100644 index 00000000000..b2b5ec070e1 --- /dev/null +++ b/qa/spec/resource/ssh_key_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +describe QA::Resource::SSHKey do + describe '#key' do + it 'generates a default key' do + expect(subject.key).to be_a(QA::Runtime::Key::RSA) + end + end + + describe '#title' do + it 'generates a default title' do + expect(subject.title).to match(/E2E test key: \d+/) + end + + it 'is possible to set the title' do + subject.title = 'I am in a title' + + expect(subject.title).to eq('E2E test key: I am in a title') + end + end +end diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb index d5c6820f0a9..1f09c3f73ac 100644 --- a/qa/spec/specs/helpers/quarantine_spec.rb +++ b/qa/spec/specs/helpers/quarantine_spec.rb @@ -31,14 +31,6 @@ RSpec.configure do |c| config.color_mode = :off - # Load airborne again to avoid "undefined method `match_expected_default?'" errors - # that happen because a hook calls a method added via a custom RSpec setting - # that is removed when the RSpec configuration is sandboxed. - # If this needs to be changed (e.g., to load other libraries as well), see - # this discussion for alternative solutions: - # https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25223#note_143392053 - load 'airborne.rb' - ex.run end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index e834cd13d36..2f5cc404143 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -31,6 +31,10 @@ FactoryBot.define do user_type { :project_bot } end + trait :migration_bot do + user_type { :migration_bot } + end + trait :external do external { true } end diff --git a/spec/frontend/pipelines/pipeline_details_mediator_spec.js b/spec/frontend/pipelines/pipeline_details_mediator_spec.js new file mode 100644 index 00000000000..083e97666ed --- /dev/null +++ b/spec/frontend/pipelines/pipeline_details_mediator_spec.js @@ -0,0 +1,36 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import PipelineMediator from '~/pipelines/pipeline_details_mediator'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('PipelineMdediator', () => { + let mediator; + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + mediator = new PipelineMediator({ endpoint: 'foo.json' }); + }); + + afterEach(() => { + mock.restore(); + }); + + it('should set defaults', () => { + expect(mediator.options).toEqual({ endpoint: 'foo.json' }); + expect(mediator.state.isLoading).toEqual(false); + expect(mediator.store).toBeDefined(); + expect(mediator.service).toBeDefined(); + }); + + describe('request and store data', () => { + it('should store received data', () => { + mock.onGet('foo.json').reply(200, { id: '121123' }); + mediator.fetchPipeline(); + + return waitForPromises().then(() => { + expect(mediator.store.state.pipeline).toEqual({ id: '121123' }); + }); + }); + }); +}); diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js deleted file mode 100644 index 5d4e3758557..00000000000 --- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js +++ /dev/null @@ -1,247 +0,0 @@ -import Vuex from 'vuex'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlSkeletonLoader } from '@gitlab/ui'; - -import createState from '~/static_site_editor/store/state'; - -import StaticSiteEditor from '~/static_site_editor/components/static_site_editor.vue'; -import EditArea from '~/static_site_editor/components/edit_area.vue'; -import EditHeader from '~/static_site_editor/components/edit_header.vue'; -import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; -import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; -import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue'; -import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue'; - -import { - returnUrl, - sourceContent, - sourceContentTitle, - savedContentMeta, - submitChangesError, -} from '../mock_data'; - -const localVue = createLocalVue(); - -localVue.use(Vuex); - -describe('StaticSiteEditor', () => { - let wrapper; - let store; - let loadContentActionMock; - let setContentActionMock; - let submitChangesActionMock; - let dismissSubmitChangesErrorActionMock; - - const buildStore = ({ initialState, getters } = {}) => { - loadContentActionMock = jest.fn(); - setContentActionMock = jest.fn(); - submitChangesActionMock = jest.fn(); - dismissSubmitChangesErrorActionMock = jest.fn(); - - store = new Vuex.Store({ - state: createState({ - isSupportedContent: true, - ...initialState, - }), - getters: { - contentChanged: () => false, - ...getters, - }, - actions: { - loadContent: loadContentActionMock, - setContent: setContentActionMock, - submitChanges: submitChangesActionMock, - dismissSubmitChangesError: dismissSubmitChangesErrorActionMock, - }, - }); - }; - const buildContentLoadedStore = ({ initialState, getters } = {}) => { - buildStore({ - initialState: { - isContentLoaded: true, - ...initialState, - }, - getters: { - ...getters, - }, - }); - }; - - const buildWrapper = () => { - wrapper = shallowMount(StaticSiteEditor, { - localVue, - store, - }); - }; - - const findEditArea = () => wrapper.find(EditArea); - const findEditHeader = () => wrapper.find(EditHeader); - const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage); - const findPublishToolbar = () => wrapper.find(PublishToolbar); - const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); - const findSubmitChangesError = () => wrapper.find(SubmitChangesError); - const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage); - - beforeEach(() => { - buildStore(); - buildWrapper(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders the saved changes message when changes are submitted successfully', () => { - buildStore({ initialState: { returnUrl, savedContentMeta } }); - buildWrapper(); - - expect(findSavedChangesMessage().exists()).toBe(true); - expect(findSavedChangesMessage().props()).toEqual({ - returnUrl, - ...savedContentMeta, - }); - }); - - describe('when content is not loaded', () => { - it('does not render edit area', () => { - expect(findEditArea().exists()).toBe(false); - }); - - it('does not render edit header', () => { - expect(findEditHeader().exists()).toBe(false); - }); - - it('does not render toolbar', () => { - expect(findPublishToolbar().exists()).toBe(false); - }); - - it('does not render saved changes message', () => { - expect(findSavedChangesMessage().exists()).toBe(false); - }); - }); - - describe('when content is loaded', () => { - const content = sourceContent; - const title = sourceContentTitle; - - beforeEach(() => { - buildContentLoadedStore({ initialState: { content, title } }); - buildWrapper(); - }); - - it('renders the edit area', () => { - expect(findEditArea().exists()).toBe(true); - }); - - it('renders the edit header', () => { - expect(findEditHeader().exists()).toBe(true); - }); - - it('does not render skeleton loader', () => { - expect(findSkeletonLoader().exists()).toBe(false); - }); - - it('passes page content to edit area', () => { - expect(findEditArea().props('value')).toBe(content); - }); - - it('passes page title to edit header', () => { - expect(findEditHeader().props('title')).toBe(title); - }); - - it('renders toolbar', () => { - expect(findPublishToolbar().exists()).toBe(true); - }); - }); - - it('sets toolbar as saveable when content changes', () => { - buildContentLoadedStore({ - getters: { - contentChanged: () => true, - }, - }); - buildWrapper(); - - expect(findPublishToolbar().props('saveable')).toBe(true); - }); - - it('displays skeleton loader when loading content', () => { - buildStore({ initialState: { isLoadingContent: true } }); - buildWrapper(); - - expect(findSkeletonLoader().exists()).toBe(true); - }); - - it('does not display submit changes error when an error does not exist', () => { - buildContentLoadedStore(); - buildWrapper(); - - expect(findSubmitChangesError().exists()).toBe(false); - }); - - it('sets toolbar as saving when saving changes', () => { - buildContentLoadedStore({ - initialState: { - isSavingChanges: true, - }, - }); - buildWrapper(); - - expect(findPublishToolbar().props('savingChanges')).toBe(true); - }); - - it('displays invalid content message when content is not supported', () => { - buildStore({ initialState: { isSupportedContent: false } }); - buildWrapper(); - - expect(findInvalidContentMessage().exists()).toBe(true); - }); - - describe('when submitting changes fail', () => { - beforeEach(() => { - buildContentLoadedStore({ - initialState: { - submitChangesError, - }, - }); - buildWrapper(); - }); - - it('displays submit changes error message', () => { - expect(findSubmitChangesError().exists()).toBe(true); - }); - - it('dispatches submitChanges action when error message emits retry event', () => { - findSubmitChangesError().vm.$emit('retry'); - - expect(submitChangesActionMock).toHaveBeenCalled(); - }); - - it('dispatches dismissSubmitChangesError action when error message emits dismiss event', () => { - findSubmitChangesError().vm.$emit('dismiss'); - - expect(dismissSubmitChangesErrorActionMock).toHaveBeenCalled(); - }); - }); - - it('dispatches load content action', () => { - expect(loadContentActionMock).toHaveBeenCalled(); - }); - - it('dispatches setContent action when edit area emits input event', () => { - buildContentLoadedStore(); - buildWrapper(); - - findEditArea().vm.$emit('input', sourceContent); - - expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined); - }); - - it('dispatches submitChanges action when toolbar emits submit event', () => { - buildContentLoadedStore(); - buildWrapper(); - findPublishToolbar().vm.$emit('submit'); - - expect(submitChangesActionMock).toHaveBeenCalled(); - }); -}); diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js new file mode 100644 index 00000000000..82e39447ae6 --- /dev/null +++ b/spec/frontend/static_site_editor/pages/home_spec.js @@ -0,0 +1,247 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlSkeletonLoader } from '@gitlab/ui'; + +import createState from '~/static_site_editor/store/state'; + +import Home from '~/static_site_editor/pages/home.vue'; +import EditArea from '~/static_site_editor/components/edit_area.vue'; +import EditHeader from '~/static_site_editor/components/edit_header.vue'; +import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; +import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; +import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue'; +import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue'; + +import { + returnUrl, + sourceContent, + sourceContentTitle, + savedContentMeta, + submitChangesError, +} from '../mock_data'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +describe('static_site_editor/pages/home', () => { + let wrapper; + let store; + let loadContentActionMock; + let setContentActionMock; + let submitChangesActionMock; + let dismissSubmitChangesErrorActionMock; + + const buildStore = ({ initialState, getters } = {}) => { + loadContentActionMock = jest.fn(); + setContentActionMock = jest.fn(); + submitChangesActionMock = jest.fn(); + dismissSubmitChangesErrorActionMock = jest.fn(); + + store = new Vuex.Store({ + state: createState({ + isSupportedContent: true, + ...initialState, + }), + getters: { + contentChanged: () => false, + ...getters, + }, + actions: { + loadContent: loadContentActionMock, + setContent: setContentActionMock, + submitChanges: submitChangesActionMock, + dismissSubmitChangesError: dismissSubmitChangesErrorActionMock, + }, + }); + }; + const buildContentLoadedStore = ({ initialState, getters } = {}) => { + buildStore({ + initialState: { + isContentLoaded: true, + ...initialState, + }, + getters: { + ...getters, + }, + }); + }; + + const buildWrapper = () => { + wrapper = shallowMount(Home, { + localVue, + store, + }); + }; + + const findEditArea = () => wrapper.find(EditArea); + const findEditHeader = () => wrapper.find(EditHeader); + const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage); + const findPublishToolbar = () => wrapper.find(PublishToolbar); + const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); + const findSubmitChangesError = () => wrapper.find(SubmitChangesError); + const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage); + + beforeEach(() => { + buildStore(); + buildWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the saved changes message when changes are submitted successfully', () => { + buildStore({ initialState: { returnUrl, savedContentMeta } }); + buildWrapper(); + + expect(findSavedChangesMessage().exists()).toBe(true); + expect(findSavedChangesMessage().props()).toEqual({ + returnUrl, + ...savedContentMeta, + }); + }); + + describe('when content is not loaded', () => { + it('does not render edit area', () => { + expect(findEditArea().exists()).toBe(false); + }); + + it('does not render edit header', () => { + expect(findEditHeader().exists()).toBe(false); + }); + + it('does not render toolbar', () => { + expect(findPublishToolbar().exists()).toBe(false); + }); + + it('does not render saved changes message', () => { + expect(findSavedChangesMessage().exists()).toBe(false); + }); + }); + + describe('when content is loaded', () => { + const content = sourceContent; + const title = sourceContentTitle; + + beforeEach(() => { + buildContentLoadedStore({ initialState: { content, title } }); + buildWrapper(); + }); + + it('renders the edit area', () => { + expect(findEditArea().exists()).toBe(true); + }); + + it('renders the edit header', () => { + expect(findEditHeader().exists()).toBe(true); + }); + + it('does not render skeleton loader', () => { + expect(findSkeletonLoader().exists()).toBe(false); + }); + + it('passes page content to edit area', () => { + expect(findEditArea().props('value')).toBe(content); + }); + + it('passes page title to edit header', () => { + expect(findEditHeader().props('title')).toBe(title); + }); + + it('renders toolbar', () => { + expect(findPublishToolbar().exists()).toBe(true); + }); + }); + + it('sets toolbar as saveable when content changes', () => { + buildContentLoadedStore({ + getters: { + contentChanged: () => true, + }, + }); + buildWrapper(); + + expect(findPublishToolbar().props('saveable')).toBe(true); + }); + + it('displays skeleton loader when loading content', () => { + buildStore({ initialState: { isLoadingContent: true } }); + buildWrapper(); + + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('does not display submit changes error when an error does not exist', () => { + buildContentLoadedStore(); + buildWrapper(); + + expect(findSubmitChangesError().exists()).toBe(false); + }); + + it('sets toolbar as saving when saving changes', () => { + buildContentLoadedStore({ + initialState: { + isSavingChanges: true, + }, + }); + buildWrapper(); + + expect(findPublishToolbar().props('savingChanges')).toBe(true); + }); + + it('displays invalid content message when content is not supported', () => { + buildStore({ initialState: { isSupportedContent: false } }); + buildWrapper(); + + expect(findInvalidContentMessage().exists()).toBe(true); + }); + + describe('when submitting changes fail', () => { + beforeEach(() => { + buildContentLoadedStore({ + initialState: { + submitChangesError, + }, + }); + buildWrapper(); + }); + + it('displays submit changes error message', () => { + expect(findSubmitChangesError().exists()).toBe(true); + }); + + it('dispatches submitChanges action when error message emits retry event', () => { + findSubmitChangesError().vm.$emit('retry'); + + expect(submitChangesActionMock).toHaveBeenCalled(); + }); + + it('dispatches dismissSubmitChangesError action when error message emits dismiss event', () => { + findSubmitChangesError().vm.$emit('dismiss'); + + expect(dismissSubmitChangesErrorActionMock).toHaveBeenCalled(); + }); + }); + + it('dispatches load content action', () => { + expect(loadContentActionMock).toHaveBeenCalled(); + }); + + it('dispatches setContent action when edit area emits input event', () => { + buildContentLoadedStore(); + buildWrapper(); + + findEditArea().vm.$emit('input', sourceContent); + + expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined); + }); + + it('dispatches submitChanges action when toolbar emits submit event', () => { + buildContentLoadedStore(); + buildWrapper(); + findPublishToolbar().vm.$emit('submit'); + + expect(submitChangesActionMock).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap index 4cd03a690e9..7dd324a61cd 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap @@ -44,18 +44,13 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` > - - + /> @@ -94,18 +89,13 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` > - - + /> diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js deleted file mode 100644 index 61ee2dc13ca..00000000000 --- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import PipelineMediator from '~/pipelines/pipeline_details_mediator'; - -describe('PipelineMdediator', () => { - let mediator; - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - mediator = new PipelineMediator({ endpoint: 'foo.json' }); - }); - - afterEach(() => { - mock.restore(); - }); - - it('should set defaults', () => { - expect(mediator.options).toEqual({ endpoint: 'foo.json' }); - expect(mediator.state.isLoading).toEqual(false); - expect(mediator.store).toBeDefined(); - expect(mediator.service).toBeDefined(); - }); - - describe('request and store data', () => { - it('should store received data', done => { - mock.onGet('foo.json').reply(200, { id: '121123' }); - mediator.fetchPipeline(); - - setTimeout(() => { - expect(mediator.store.state.pipeline).toEqual({ id: '121123' }); - done(); - }, 0); - }); - }); -}); diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb index cf12f263bd7..7ec29dde57b 100644 --- a/spec/models/concerns/has_user_type_spec.rb +++ b/spec/models/concerns/has_user_type_spec.rb @@ -4,8 +4,8 @@ require 'spec_helper' describe User do specify 'types consistency checks', :aggregate_failures do - expect(described_class::USER_TYPES) - .to include(*%i[human ghost alert_bot project_bot support_bot service_user visual_review_bot]) + expect(described_class::USER_TYPES.keys) + .to match_array(%w[human ghost alert_bot project_bot support_bot service_user visual_review_bot migration_bot]) expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES) expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES) expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b05311814d0..7649b09aa8e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -4584,4 +4584,20 @@ describe User, :do_not_mock_admin_mode do it_behaves_like 'does not require password to be present' end end + + describe '#migration_bot' do + it 'creates the user if it does not exist' do + expect do + described_class.migration_bot + end.to change { User.where(user_type: :migration_bot).count }.by(1) + end + + it 'does not create a new user if it already exists' do + described_class.migration_bot + + expect do + described_class.migration_bot + end.not_to change { User.count } + end + end end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index ac7f5db980c..bd0722ce20a 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -6,6 +6,7 @@ describe GlobalPolicy do include TermsHelper let_it_be(:project_bot) { create(:user, :project_bot) } + let_it_be(:migration_bot) { create(:user, :migration_bot) } let(:current_user) { create(:user) } let(:user) { create(:user) } @@ -155,6 +156,12 @@ describe GlobalPolicy do it { is_expected.to be_allowed(:access_api) } end + context 'migration bot' do + let(:current_user) { migration_bot } + + it { is_expected.not_to be_allowed(:access_api) } + end + context 'when terms are enforced' do before do enforce_terms @@ -244,6 +251,12 @@ describe GlobalPolicy do it { is_expected.not_to be_allowed(:receive_notifications) } end + + context 'migration bot' do + let(:current_user) { migration_bot } + + it { is_expected.not_to be_allowed(:receive_notifications) } + end end describe 'git access' do @@ -263,6 +276,12 @@ describe GlobalPolicy do it { is_expected.to be_allowed(:access_git) } end + context 'migration bot' do + let(:current_user) { migration_bot } + + it { is_expected.to be_allowed(:access_git) } + end + describe 'deactivated user' do before do current_user.deactivate @@ -414,6 +433,12 @@ describe GlobalPolicy do it { is_expected.to be_allowed(:use_slash_commands) } end + + context 'migration bot' do + let(:current_user) { migration_bot } + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end end describe 'create_snippet' do @@ -440,5 +465,11 @@ describe GlobalPolicy do it { is_expected.not_to be_allowed(:log_in) } end + + context 'migration bot' do + let(:current_user) { migration_bot } + + it { is_expected.not_to be_allowed(:log_in) } + end end end diff --git a/spec/views/admin/users/_user.html.haml_spec.rb b/spec/views/admin/users/_user.html.haml_spec.rb index 0eaaa2d5304..de5a291a6f8 100644 --- a/spec/views/admin/users/_user.html.haml_spec.rb +++ b/spec/views/admin/users/_user.html.haml_spec.rb @@ -27,6 +27,16 @@ describe 'admin/users/_user.html.haml' do expect(rendered).not_to have_selector('.table-action-buttons') end end + + context 'when showing a `Migration User`' do + let(:user) { create(:user, user_type: :migration_bot) } + + it 'does not render action buttons' do + render + + expect(rendered).not_to have_selector('.table-action-buttons') + end + end end context 'when showing an external user' do -- cgit v1.2.1