diff options
19 files changed, 591 insertions, 330 deletions
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 36701a95673..b4b124d5db1 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -30,7 +30,7 @@ export default { EnvironmentsBlock, ErasedBlock, Icon, - Log: () => (isNewJobLogActive() ? import('./job_log_json.vue') : import('./job_log.vue')), + Log: () => (isNewJobLogActive() ? import('./log/log.vue') : import('./job_log.vue')), LogTopBar, StuckBlock, UnmetPrerequisitesBlock, diff --git a/app/assets/javascripts/jobs/components/job_log_json.vue b/app/assets/javascripts/jobs/components/job_log_json.vue deleted file mode 100644 index 2198b20eb8f..00000000000 --- a/app/assets/javascripts/jobs/components/job_log_json.vue +++ /dev/null @@ -1,10 +0,0 @@ -<script> -export default { - name: 'JobLogJSON', -}; -</script> -<template> - <pre> - {{ __('This feature is in development. Please disable the `job_log_json` feature flag') }} - </pre> -</template> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ad242a078ad..01d80d77080 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base include SessionlessAuthentication include ConfirmEmailWarning include Gitlab::Tracking::ControllerConcern + include Gitlab::Experimentation::ControllerConcern before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? diff --git a/app/services/projects/after_import_service.rb b/app/services/projects/after_import_service.rb index e30da0f26df..6fc15db9b4c 100644 --- a/app/services/projects/after_import_service.rb +++ b/app/services/projects/after_import_service.rb @@ -9,9 +9,16 @@ module Projects end def execute - Projects::HousekeepingService.new(@project).execute do + service = Projects::HousekeepingService.new(@project) + + service.execute do repository.delete_all_refs_except(RESERVED_REF_PREFIXES) end + + # Right now we don't actually have a way to know if a project + # import actually changed, so we increment the counter to avoid + # causing GC to run every time. + service.increment! rescue Projects::HousekeepingService::LeaseTaken => e Rails.logger.info( # rubocop:disable Gitlab/RailsLogger "Could not perform housekeeping for project #{@project.full_path} (#{@project.id}): #{e}") diff --git a/app/views/admin/application_settings/_protected_paths.html.haml b/app/views/admin/application_settings/_protected_paths.html.haml index cfb04562b59..f4d40e10f36 100644 --- a/app/views/admin/application_settings/_protected_paths.html.haml +++ b/app/views/admin/application_settings/_protected_paths.html.haml @@ -4,7 +4,7 @@ %fieldset - if omnibus_protected_paths_throttle? .bs-callout.bs-callout-danger - - relative_url_link = 'https://docs.gitlab.com/ee/user/admin_area/settings/protected_paths.html#migrating-from-omnibus' + - relative_url_link = 'https://docs.gitlab.com/ee/user/admin_area/settings/protected_paths.html#migrate-settings-from-gitlab-123-and-earlier' - relative_url_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: relative_url_link } = _("Omnibus Protected Paths throttle is active. From 12.4, Omnibus throttle is deprecated and will be removed in a future release. Please read the %{relative_url_link_start}Migrating Protected Paths documentation%{relative_url_link_end}.").html_safe % { relative_url_link_start: relative_url_link_start, relative_url_link_end: '</a>'.html_safe } diff --git a/changelogs/unreleased/mc-fixes-wrong-link-on-protected-paths-admin-ui.yml b/changelogs/unreleased/mc-fixes-wrong-link-on-protected-paths-admin-ui.yml new file mode 100644 index 00000000000..61e54f2f5e0 --- /dev/null +++ b/changelogs/unreleased/mc-fixes-wrong-link-on-protected-paths-admin-ui.yml @@ -0,0 +1,5 @@ +--- +title: Fixes wrong link on Protected paths admin settings +merge_request: 17945 +author: +type: other diff --git a/doc/api/epics.md b/doc/api/epics.md index 7ef12766f78..c24df6a236f 100644 --- a/doc/api/epics.md +++ b/doc/api/epics.md @@ -88,6 +88,7 @@ Example response: "due_date_from_milestones": "2018-07-31", "created_at": "2018-07-17T13:36:22.770Z", "updated_at": "2018-07-18T12:22:05.239Z", + "closed_at": "2018-08-18T12:22:05.239Z", "labels": [], "upvotes": 4, "downvotes": 0 @@ -143,6 +144,7 @@ Example response: "due_date_from_milestones": "2018-07-31", "created_at": "2018-07-17T13:36:22.770Z", "updated_at": "2018-07-18T12:22:05.239Z", + "closed_at": "2018-08-18T12:22:05.239Z", "labels": [], "upvotes": 4, "downvotes": 0 @@ -209,6 +211,7 @@ Example response: "due_date_from_milestones": "2018-07-31", "created_at": "2018-07-17T13:36:22.770Z", "updated_at": "2018-07-18T12:22:05.239Z", + "closed_at": "2018-08-18T12:22:05.239Z", "labels": [], "upvotes": 4, "downvotes": 0 @@ -276,6 +279,7 @@ Example response: "due_date_from_milestones": "2018-07-31", "created_at": "2018-07-17T13:36:22.770Z", "updated_at": "2018-07-18T12:22:05.239Z", + "closed_at": "2018-08-18T12:22:05.239Z", "labels": [], "upvotes": 4, "downvotes": 0 @@ -358,7 +362,8 @@ Example response: "start_date": null, "end_date": null, "created_at": "2018-01-21T06:21:13.165Z", - "updated_at": "2018-01-22T12:41:41.166Z" + "updated_at": "2018-01-22T12:41:41.166Z", + "closed_at": "2018-08-18T12:22:05.239Z" }, "target_url": "https://gitlab.example.com/groups/epics/5", "body": "Vel voluptas atque dicta mollitia adipisci qui at.", diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md index 41513e6d57e..21e0c869f13 100644 --- a/doc/development/fe_guide/development_process.md +++ b/doc/development/fe_guide/development_process.md @@ -80,7 +80,7 @@ With the purpose of being [respectful of others' time](https://about.gitlab.com/ 1. Before writing code, ensure your vision of the architecture is aligned with GitLab's architecture. -1. Add a diagram to the issue and ask a frontend architect in the slack channel `#fe_architectural` about it. +1. Add a diagram to the issue and ask a frontend maintainer in the slack channel `#frontend_maintainers` about it. ![Diagram of Issue Boards Architecture](img/boards_diagram.png) diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md index 22e773e8ea3..fe4f6d7bec8 100644 --- a/doc/development/fe_guide/graphql.md +++ b/doc/development/fe_guide/graphql.md @@ -1,11 +1,18 @@ # GraphQL +Our GraphQL API can be explored via GraphiQL at your instance's +`/-/graphql-explorer` or at [GitLab.com](https://gitlab.com/-/graphql-explorer). + +You can check all existing queries and mutations on the right side +of GraphiQL in its **Documentation explorer**. It's also possible to +write queries and mutations directly on the left tab and check +their execution by clicking **Execute query** button on the top left: + +![GraphiQL interface](img/graphiql_explorer_v12_4.png) + We use [Apollo] and [Vue Apollo][vue-apollo] for working with GraphQL on the frontend. -In order to use GraphQL, you need to enable the `graphql` feature flag, -read more about [Feature Flags][feature-flags]. - ## Apollo Client To save duplicated clients getting created in different apps, we have a diff --git a/doc/development/fe_guide/img/graphiql_explorer_v12_4.png b/doc/development/fe_guide/img/graphiql_explorer_v12_4.png Binary files differnew file mode 100644 index 00000000000..8981b37ba23 --- /dev/null +++ b/doc/development/fe_guide/img/graphiql_explorer_v12_4.png diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb new file mode 100644 index 00000000000..678d47150e8 --- /dev/null +++ b/lib/gitlab/experimentation.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# == Experimentation +# +# Utility module used for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant. +# The feature_toggle and environment keys are optional. If the feature_toggle is not set, a feature with the name of +# the experiment will be checked, with a default value of true. The enabled_ratio is required and should be +# the ratio for the number of users for which this experiment is enabled. For example: a ratio of 0.1 will +# enable the experiment for 10% of the users (determined by the `experimentation_subject_index`). +# +module Gitlab + module Experimentation + EXPERIMENTS = { + signup_flow: { + feature_toggle: :experimental_separate_sign_up_flow, + environment: ::Gitlab.dev_env_or_com?, + enabled_ratio: 0.1 + } + }.freeze + + # Controller concern that checks if an experimentation_subject_id cookie is present and sets it if absent. + # Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method + # to controllers and views. + # + module ControllerConcern + extend ActiveSupport::Concern + + included do + before_action :set_experimentation_subject_id_cookie + helper_method :experiment_enabled? + end + + def set_experimentation_subject_id_cookie + return if cookies[:experimentation_subject_id].present? + + cookies.permanent.signed[:experimentation_subject_id] = { + value: SecureRandom.uuid, + domain: :all, + secure: ::Gitlab.config.gitlab.https + } + end + + def experiment_enabled?(experiment) + Experimentation.enabled?(experiment, experimentation_subject_index) + end + + private + + def experimentation_subject_index + experimentation_subject_id = cookies.signed[:experimentation_subject_id] + return if experimentation_subject_id.blank? + + experimentation_subject_id.delete('-').hex % 100 + end + end + + class << self + def enabled?(experiment_key, experimentation_subject_index) + return false unless EXPERIMENTS.key?(experiment_key) + + experiment = Experiment.new(EXPERIMENTS[experiment_key].merge(key: experiment_key)) + + experiment.feature_toggle_enabled? && + experiment.enabled_for_environment? && + experiment.enabled_for_experimentation_subject?(experimentation_subject_index) + end + end + + Experiment = Struct.new(:key, :feature_toggle, :environment, :enabled_ratio, keyword_init: true) do + def feature_toggle_enabled? + return Feature.enabled?(key, default_enabled: true) if feature_toggle.nil? + + Feature.enabled?(feature_toggle) + end + + def enabled_for_environment? + return true if environment.nil? + + environment + end + + def enabled_for_experimentation_subject?(experimentation_subject_index) + return false if enabled_ratio.nil? || experimentation_subject_index.blank? + + experimentation_subject_index <= enabled_ratio * 100 + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8eb7d4d5a8d..8a00584894b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16074,9 +16074,6 @@ msgstr "" msgid "This environment has no deployments yet." msgstr "" -msgid "This feature is in development. Please disable the `job_log_json` feature flag" -msgstr "" - msgid "This feature requires local storage to be enabled" msgstr "" diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb index e30b28a4bd5..6ed822bbb10 100644 --- a/spec/controllers/projects/discussions_controller_spec.rb +++ b/spec/controllers/projects/discussions_controller_spec.rb @@ -189,7 +189,7 @@ describe Projects::DiscussionsController do context "when vue_mr_discussions cookie is present" do before do - allow(controller).to receive(:cookies).and_return({ vue_mr_discussions: 'true' }) + cookies[:vue_mr_discussions] = 'true' end it "renders discussion with serializer" do diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js new file mode 100644 index 00000000000..5cfe1c25c6b --- /dev/null +++ b/spec/frontend/ide/components/branches/search_list_spec.js @@ -0,0 +1,81 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { __ } from '~/locale'; +import List from '~/ide/components/branches/search_list.vue'; +import Item from '~/ide/components/branches/item.vue'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { branches } from '../../mock_data'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('IDE branches search list', () => { + let wrapper; + const fetchBranchesMock = jest.fn(); + + const createComponent = (state, currentBranchId = 'branch') => { + const fakeStore = new Vuex.Store({ + state: { + currentBranchId, + currentProjectId: 'project', + }, + modules: { + branches: { + namespaced: true, + state: { isLoading: false, branches: [], ...state }, + actions: { + fetchBranches: fetchBranchesMock, + }, + }, + }, + }); + + wrapper = shallowMount(List, { + localVue, + store: fakeStore, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('calls fetch on mounted', () => { + createComponent(); + expect(fetchBranchesMock).toHaveBeenCalled(); + }); + + it('renders loading icon when `isLoading` is true', () => { + createComponent({ isLoading: true }); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('renders branches not found when search is not empty and branches list is empty', () => { + createComponent({ branches: [] }); + wrapper.find('input[type="search"]').setValue('something'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.text()).toContain(__('No branches found')); + }); + }); + + describe('with branches', () => { + it('renders list', () => { + createComponent({ branches }); + const items = wrapper.findAll(Item); + + expect(items.length).toBe(branches.length); + }); + + it('renders check next to active branch', () => { + const activeBranch = 'regular'; + createComponent({ branches }, activeBranch); + const items = wrapper.findAll(Item).filter(w => w.props('isActive')); + + expect(items.length).toBe(1); + expect(items.at(0).props('item').name).toBe(activeBranch); + }); + }); +}); diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js new file mode 100644 index 00000000000..80eb15fe5a6 --- /dev/null +++ b/spec/frontend/ide/mock_data.js @@ -0,0 +1,228 @@ +import { TEST_HOST } from 'spec/test_constants'; + +export const projectData = { + id: 1, + name: 'abcproject', + web_url: '', + avatar_url: '', + path: '', + name_with_namespace: 'namespace/abcproject', + branches: { + master: { + treeId: 'abcproject/master', + can_push: true, + commit: { + id: '123', + }, + }, + }, + mergeRequests: {}, + merge_requests_enabled: true, + default_branch: 'master', +}; + +export const pipelines = [ + { + id: 1, + ref: 'master', + sha: '123', + details: { + status: { + icon: 'status_failed', + group: 'failed', + text: 'Failed', + }, + }, + commit: { id: '123' }, + }, + { + id: 2, + ref: 'master', + sha: '213', + details: { + status: { + icon: 'status_failed', + group: 'failed', + text: 'Failed', + }, + }, + commit: { id: '213' }, + }, +]; + +export const stages = [ + { + dropdown_path: `${TEST_HOST}/testing`, + name: 'build', + status: { + icon: 'status_failed', + group: 'failed', + text: 'failed', + }, + }, + { + dropdown_path: 'testing', + name: 'test', + status: { + icon: 'status_failed', + group: 'failed', + text: 'failed', + }, + }, +]; + +export const jobs = [ + { + id: 1, + name: 'test', + path: 'testing', + status: { + icon: 'status_success', + text: 'passed', + }, + stage: 'test', + duration: 1, + started: new Date(), + }, + { + id: 2, + name: 'test 2', + path: 'testing2', + status: { + icon: 'status_success', + text: 'passed', + }, + stage: 'test', + duration: 1, + started: new Date(), + }, + { + id: 3, + name: 'test 3', + path: 'testing3', + status: { + icon: 'status_success', + text: 'passed', + }, + stage: 'test', + duration: 1, + started: new Date(), + }, + { + id: 4, + name: 'test 4', + path: 'testing4', + status: { + icon: 'status_failed', + text: 'failed', + }, + stage: 'build', + duration: 1, + started: new Date(), + }, +]; + +export const fullPipelinesResponse = { + data: { + count: { + all: 2, + }, + pipelines: [ + { + id: '51', + path: 'test', + commit: { + id: '123', + }, + details: { + status: { + icon: 'status_failed', + text: 'failed', + }, + stages: [...stages], + }, + }, + { + id: '50', + commit: { + id: 'abc123def456ghi789jkl', + }, + details: { + status: { + icon: 'status_success', + text: 'passed', + }, + stages: [...stages], + }, + }, + ], + }, +}; + +export const mergeRequests = [ + { + id: 1, + iid: 1, + title: 'Test merge request', + project_id: 1, + web_url: `${TEST_HOST}/namespace/project-path/merge_requests/1`, + }, +]; + +export const branches = [ + { + id: 1, + name: 'master', + commit: { + message: 'Update master branch', + committed_date: '2018-08-01T00:20:05Z', + }, + can_push: true, + protected: true, + default: true, + }, + { + id: 2, + name: 'protected/no-access', + commit: { + message: 'Update some stuff', + committed_date: '2018-08-02T00:00:05Z', + }, + can_push: false, + protected: true, + default: false, + }, + { + id: 3, + name: 'protected/access', + commit: { + message: 'Update some stuff', + committed_date: '2018-08-02T00:00:05Z', + }, + can_push: true, + protected: true, + default: false, + }, + { + id: 4, + name: 'regular', + commit: { + message: 'Update some more stuff', + committed_date: '2018-06-30T00:20:05Z', + }, + can_push: true, + protected: false, + default: false, + }, + { + id: 5, + name: 'regular/no-access', + commit: { + message: 'Update some more stuff', + committed_date: '2018-06-30T00:20:05Z', + }, + can_push: false, + protected: false, + default: false, + }, +]; diff --git a/spec/javascripts/ide/components/branches/search_list_spec.js b/spec/javascripts/ide/components/branches/search_list_spec.js deleted file mode 100644 index 72a3c2d5dcd..00000000000 --- a/spec/javascripts/ide/components/branches/search_list_spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import * as types from '~/ide/stores/modules/branches/mutation_types'; -import List from '~/ide/components/branches/search_list.vue'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; -import { branches as testBranches } from '../../mock_data'; -import { resetStore } from '../../helpers'; - -describe('IDE branches search list', () => { - const Component = Vue.extend(List); - let vm; - - beforeEach(() => { - vm = createComponentWithStore(Component, store, {}); - - spyOn(vm, 'fetchBranches'); - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(store); - }); - - it('calls fetch on mounted', () => { - expect(vm.fetchBranches).toHaveBeenCalledWith({ - search: '', - }); - }); - - it('renders loading icon', done => { - vm.$store.state.branches.isLoading = true; - - vm.$nextTick() - .then(() => { - expect(vm.$el).toContainElement('.loading-container'); - }) - .then(done) - .catch(done.fail); - }); - - it('renders branches not found when search is not empty', done => { - vm.search = 'testing'; - - vm.$nextTick(() => { - expect(vm.$el).toContainText('No branches found'); - - done(); - }); - }); - - describe('with branches', () => { - const currentBranch = testBranches[1]; - - beforeEach(done => { - vm.$store.state.currentBranchId = currentBranch.name; - vm.$store.commit(`branches/${types.RECEIVE_BRANCHES_SUCCESS}`, testBranches); - - vm.$nextTick(done); - }); - - it('renders list', () => { - const elementText = Array.from(vm.$el.querySelectorAll('li strong')).map(x => - x.textContent.trim(), - ); - - expect(elementText).toEqual(testBranches.map(x => x.name)); - }); - - it('renders check next to active branch', () => { - const checkedText = Array.from(vm.$el.querySelectorAll('li')) - .filter(x => x.querySelector('.ide-search-list-current-icon svg')) - .map(x => x.querySelector('strong').textContent.trim()); - - expect(checkedText).toEqual([currentBranch.name]); - }); - }); -}); diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index 1c2e082489e..27f0ad01f54 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -1,228 +1 @@ -import { TEST_HOST } from '../test_constants'; - -export const projectData = { - id: 1, - name: 'abcproject', - web_url: '', - avatar_url: '', - path: '', - name_with_namespace: 'namespace/abcproject', - branches: { - master: { - treeId: 'abcproject/master', - can_push: true, - commit: { - id: '123', - }, - }, - }, - mergeRequests: {}, - merge_requests_enabled: true, - default_branch: 'master', -}; - -export const pipelines = [ - { - id: 1, - ref: 'master', - sha: '123', - details: { - status: { - icon: 'status_failed', - group: 'failed', - text: 'Failed', - }, - }, - commit: { id: '123' }, - }, - { - id: 2, - ref: 'master', - sha: '213', - details: { - status: { - icon: 'status_failed', - group: 'failed', - text: 'Failed', - }, - }, - commit: { id: '213' }, - }, -]; - -export const stages = [ - { - dropdown_path: `${TEST_HOST}/testing`, - name: 'build', - status: { - icon: 'status_failed', - group: 'failed', - text: 'failed', - }, - }, - { - dropdown_path: 'testing', - name: 'test', - status: { - icon: 'status_failed', - group: 'failed', - text: 'failed', - }, - }, -]; - -export const jobs = [ - { - id: 1, - name: 'test', - path: 'testing', - status: { - icon: 'status_success', - text: 'passed', - }, - stage: 'test', - duration: 1, - started: new Date(), - }, - { - id: 2, - name: 'test 2', - path: 'testing2', - status: { - icon: 'status_success', - text: 'passed', - }, - stage: 'test', - duration: 1, - started: new Date(), - }, - { - id: 3, - name: 'test 3', - path: 'testing3', - status: { - icon: 'status_success', - text: 'passed', - }, - stage: 'test', - duration: 1, - started: new Date(), - }, - { - id: 4, - name: 'test 4', - path: 'testing4', - status: { - icon: 'status_failed', - text: 'failed', - }, - stage: 'build', - duration: 1, - started: new Date(), - }, -]; - -export const fullPipelinesResponse = { - data: { - count: { - all: 2, - }, - pipelines: [ - { - id: '51', - path: 'test', - commit: { - id: '123', - }, - details: { - status: { - icon: 'status_failed', - text: 'failed', - }, - stages: [...stages], - }, - }, - { - id: '50', - commit: { - id: 'abc123def456ghi789jkl', - }, - details: { - status: { - icon: 'status_success', - text: 'passed', - }, - stages: [...stages], - }, - }, - ], - }, -}; - -export const mergeRequests = [ - { - id: 1, - iid: 1, - title: 'Test merge request', - project_id: 1, - web_url: `${TEST_HOST}/namespace/project-path/merge_requests/1`, - }, -]; - -export const branches = [ - { - id: 1, - name: 'master', - commit: { - message: 'Update master branch', - committed_date: '2018-08-01T00:20:05Z', - }, - can_push: true, - protected: true, - default: true, - }, - { - id: 2, - name: 'protected/no-access', - commit: { - message: 'Update some stuff', - committed_date: '2018-08-02T00:00:05Z', - }, - can_push: false, - protected: true, - default: false, - }, - { - id: 3, - name: 'protected/access', - commit: { - message: 'Update some stuff', - committed_date: '2018-08-02T00:00:05Z', - }, - can_push: true, - protected: true, - default: false, - }, - { - id: 4, - name: 'regular', - commit: { - message: 'Update some more stuff', - committed_date: '2018-06-30T00:20:05Z', - }, - can_push: true, - protected: false, - default: false, - }, - { - id: 5, - name: 'regular/no-access', - commit: { - message: 'Update some more stuff', - committed_date: '2018-06-30T00:20:05Z', - }, - can_push: false, - protected: false, - default: false, - }, -]; +export * from '../../frontend/ide/mock_data'; diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb new file mode 100644 index 00000000000..4d473731f39 --- /dev/null +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Experimentation::ControllerConcern, type: :controller do + controller(ApplicationController) do + include Gitlab::Experimentation::ControllerConcern + + def index + head :ok + end + end + + describe '#set_experimentation_subject_id_cookie' do + before do + get :index + end + + context 'cookie is present' do + before do + cookies[:experimentation_subject_id] = 'test' + end + + it 'does not change the cookie' do + expect(cookies[:experimentation_subject_id]).to eq 'test' + end + end + + context 'cookie is not present' do + it 'sets a permanent signed cookie' do + expect(cookies.permanent.signed[:experimentation_subject_id]).to be_present + end + end + end + + describe '#experiment_enabled?' do + context 'cookie is not present' do + it 'calls Gitlab::Experimentation.enabled? with the name of the experiment and an experimentation_subject_index of nil' do + expect(Gitlab::Experimentation).to receive(:enabled?).with(:test_experiment, nil) + controller.experiment_enabled?(:test_experiment) + end + end + + context 'cookie is present' do + before do + cookies.permanent.signed[:experimentation_subject_id] = 'abcd-1234' + get :index + end + + it 'calls Gitlab::Experimentation.enabled? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do + # 'abcd1234'.hex % 100 = 76 + expect(Gitlab::Experimentation).to receive(:enabled?).with(:test_experiment, 76) + controller.experiment_enabled?(:test_experiment) + end + end + end +end + +describe Gitlab::Experimentation do + before do + stub_const('Gitlab::Experimentation::EXPERIMENTS', { + test_experiment: { + feature_toggle: feature_toggle, + environment: environment, + enabled_ratio: enabled_ratio + } + }) + + stub_feature_flags(feature_toggle => true) + end + + let(:feature_toggle) { :test_experiment_toggle } + let(:environment) { Rails.env.test? } + let(:enabled_ratio) { 0.1 } + + describe '.enabled?' do + subject { described_class.enabled?(:test_experiment, experimentation_subject_index) } + let(:experimentation_subject_index) { 9 } + + context 'feature toggle is enabled, we are on the right environment and we are selected' do + it { is_expected.to be_truthy } + end + + describe 'experiment is not defined' do + it 'returns false' do + expect(described_class.enabled?(:missing_experiment, experimentation_subject_index)).to be_falsey + end + end + + describe 'feature toggle' do + context 'feature toggle is not set' do + let(:feature_toggle) { nil } + + it { is_expected.to be_truthy } + end + + context 'feature toggle is not set, but a feature with the experiment key as name does exist' do + before do + stub_feature_flags(test_experiment: false) + end + + let(:feature_toggle) { nil } + + it { is_expected.to be_falsey } + end + + context 'feature toggle is disabled' do + before do + stub_feature_flags(feature_toggle => false) + end + + it { is_expected.to be_falsey } + end + end + + describe 'environment' do + context 'environment is not set' do + let(:environment) { nil } + + it { is_expected.to be_truthy } + end + + context 'we are on the wrong environment' do + let(:environment) { ::Gitlab.com? } + + it { is_expected.to be_falsey } + end + end + + describe 'enabled ratio' do + context 'enabled ratio is not set' do + let(:enabled_ratio) { nil } + + it { is_expected.to be_falsey } + end + + context 'experimentation_subject_index is not set' do + let(:experimentation_subject_index) { nil } + + it { is_expected.to be_falsey } + end + + context 'experimentation_subject_index is an empty string' do + let(:experimentation_subject_index) { '' } + + it { is_expected.to be_falsey } + end + + context 'experimentation_subject_index outside enabled ratio' do + let(:experimentation_subject_index) { 11 } + + it { is_expected.to be_falsey } + end + end + end +end diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb index 51d3fd18881..27e8f3c45ba 100644 --- a/spec/services/projects/after_import_service_spec.rb +++ b/spec/services/projects/after_import_service_spec.rb @@ -19,6 +19,8 @@ describe Projects::AfterImportService do allow(housekeeping_service) .to receive(:execute).and_yield + + expect(housekeeping_service).to receive(:increment!) end it 'performs housekeeping' do |