summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb3
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb2
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb3
-rw-r--r--spec/controllers/projects/services_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb8
-rw-r--r--spec/factories/ci/runner_projects.rb2
-rw-r--r--spec/factories/ci/runners.rb31
-rw-r--r--spec/factories/clusters/applications/helm.rb3
-rw-r--r--spec/factories/issues.rb1
-rw-r--r--spec/features/admin/admin_runners_spec.rb19
-rw-r--r--spec/features/boards/issue_ordering_spec.rb7
-rw-r--r--spec/features/runners_spec.rb27
-rw-r--r--spec/finders/runner_jobs_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json3
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json2
-rw-r--r--spec/initializers/secret_token_spec.rb2
-rw-r--r--spec/javascripts/boards/board_list_spec.js2
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js32
-rw-r--r--spec/javascripts/clusters/components/application_row_spec.js22
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js94
-rw-r--r--spec/javascripts/clusters/services/mock_data.js35
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js18
-rw-r--r--spec/javascripts/ide/components/jobs/item_spec.js29
-rw-r--r--spec/javascripts/ide/components/jobs/list_spec.js67
-rw-r--r--spec/javascripts/ide/components/jobs/stage_spec.js95
-rw-r--r--spec/javascripts/ide/components/pipelines/list_spec.js117
-rw-r--r--spec/javascripts/ide/helpers.js4
-rw-r--r--spec/javascripts/ide/mock_data.js80
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js111
-rw-r--r--spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js182
-rw-r--r--spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js55
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/actions_spec.js265
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/getters_spec.js31
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js162
-rw-r--r--spec/javascripts/ide/stores/mutations/branch_spec.js36
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js23
-rw-r--r--spec/javascripts/vue_shared/components/tabs/tab_spec.js32
-rw-r--r--spec/javascripts/vue_shared/components/tabs/tabs_spec.js68
-rw-r--r--spec/lib/backup/repository_spec.rb2
-rw-r--r--spec/lib/feature_spec.rb24
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb12
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb4
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb133
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb10
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb6
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb10
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/models/ci/runner_spec.rb199
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb59
-rw-r--r--spec/models/clusters/cluster_spec.rb3
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb133
-rw-r--r--spec/models/concerns/has_variable_spec.rb4
-rw-r--r--spec/models/concerns/issuable_spec.rb5
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb1
-rw-r--r--spec/models/project_spec.rb14
-rw-r--r--spec/models/remote_mirror_spec.rb2
-rw-r--r--spec/models/timelog_spec.rb3
-rw-r--r--spec/models/user_spec.rb40
-rw-r--r--spec/requests/api/commit_statuses_spec.rb2
-rw-r--r--spec/requests/api/issues_spec.rb14
-rw-r--r--spec/requests/api/runner_spec.rb18
-rw-r--r--spec/requests/api/runners_spec.rb81
-rw-r--r--spec/serializers/runner_entity_spec.rb4
-rw-r--r--spec/services/ci/register_job_service_spec.rb42
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb17
-rw-r--r--spec/services/issues/update_service_spec.rb8
-rw-r--r--spec/services/merge_requests/update_service_spec.rb8
-rw-r--r--spec/support/gitaly.rb5
-rw-r--r--spec/support/helpers/drag_to_helper.rb4
-rw-r--r--spec/support/shared_examples/url_validator_examples.rb42
-rw-r--r--spec/validators/public_url_validator_spec.rb28
-rw-r--r--spec/validators/url_placeholder_validator_spec.rb39
-rw-r--r--spec/validators/url_validator_spec.rb68
75 files changed, 1911 insertions, 816 deletions
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index 6d31b0ce959..5770d15557c 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Groups::RunnersController do
let(:user) { create(:user) }
let(:group) { create(:group) }
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
let(:params) do
{
@@ -15,7 +15,6 @@ describe Groups::RunnersController do
before do
sign_in(user)
group.add_master(user)
- group.runners << runner
end
describe '#update' do
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index 45c1218a39c..5d64f362252 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -54,7 +54,7 @@ describe Projects::MirrorsController do
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
expect(response).to redirect_to(project_settings_repository_path(project))
- expect(flash[:alert]).to match(/must be a valid URL/)
+ expect(flash[:alert]).to match(/Only allowed protocols are/)
end
it 'should not create a RemoteMirror object' do
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
index 89a13f3c976..2082dd2cff0 100644
--- a/spec/controllers/projects/runners_controller_spec.rb
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::RunnersController do
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:params) do
{
@@ -16,7 +16,6 @@ describe Projects::RunnersController do
before do
sign_in(user)
project.add_master(user)
- project.runners << runner
end
describe '#update' do
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index e4dc61b3a68..61f35cf325b 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -102,7 +102,7 @@ describe Projects::ServicesController do
expect(response.status).to eq(200)
expect(JSON.parse(response.body))
- .to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test')
+ .to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test', 'test_failed' => true)
end
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index f1810763d2d..d53fe9bf734 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -19,12 +19,12 @@ describe Projects::Settings::CiCdController do
end
context 'with group runners' do
- let(:group_runner) { create(:ci_runner, runner_type: :group_type) }
let(:parent_group) { create(:group) }
- let(:group) { create(:group, runners: [group_runner], parent: parent_group) }
+ let(:group) { create(:group, parent: parent_group) }
+ let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let(:other_project) { create(:project, group: group) }
- let!(:project_runner) { create(:ci_runner, projects: [other_project], runner_type: :project_type) }
- let!(:shared_runner) { create(:ci_runner, :shared) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [other_project]) }
+ let!(:shared_runner) { create(:ci_runner, :instance) }
it 'sets assignable project runners only' do
group.add_master(user)
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index f605e90ceed..ec15972c423 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,6 +1,6 @@
FactoryBot.define do
factory :ci_runner_project, class: Ci::RunnerProject do
- runner factory: :ci_runner
+ runner factory: [:ci_runner, :project]
project
end
end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index cdc170b9ccb..6fb621b5e51 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -3,22 +3,45 @@ FactoryBot.define do
sequence(:description) { |n| "My runner#{n}" }
platform "darwin"
- is_shared false
active true
access_level :not_protected
- runner_type :project_type
+
+ is_shared true
+ runner_type :instance_type
trait :online do
contacted_at Time.now
end
- trait :shared do
+ trait :instance do
is_shared true
runner_type :instance_type
end
- trait :specific do
+ trait :group do
+ is_shared false
+ runner_type :group_type
+
+ after(:build) do |runner, evaluator|
+ runner.groups << build(:group) if runner.groups.empty?
+ end
+ end
+
+ trait :project do
is_shared false
+ runner_type :project_type
+
+ after(:build) do |runner, evaluator|
+ runner.projects << build(:project) if runner.projects.empty?
+ end
+ end
+
+ trait :without_projects do
+ # we use that to create invalid runner:
+ # the one without projects
+ after(:create) do |runner, evaluator|
+ runner.runner_projects.delete_all
+ end
end
trait :inactive do
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 3deca103578..3e4277e4ba6 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -35,5 +35,8 @@ FactoryBot.define do
factory :clusters_applications_ingress, class: Clusters::Applications::Ingress
factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus
factory :clusters_applications_runner, class: Clusters::Applications::Runner
+ factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do
+ oauth_application factory: :oauth_application
+ end
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 998080a3dd5..3a35bdd25de 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -3,6 +3,7 @@ FactoryBot.define do
title { generate(:title) }
project
author { project.creator }
+ updated_by { author }
trait :confidential do
confidential true
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index c33014cbb31..be8754a5315 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -62,7 +62,7 @@ describe "Admin Runners" do
context 'group runner' do
let(:group) { create(:group) }
- let!(:runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
+ let!(:runner) { create(:ci_runner, :group, groups: [group]) }
it 'shows the label and does not show the project count' do
visit admin_runners_path
@@ -76,7 +76,7 @@ describe "Admin Runners" do
context 'shared runner' do
it 'shows the label and does not show the project count' do
- runner = create :ci_runner, :shared
+ runner = create :ci_runner, :instance
visit admin_runners_path
@@ -90,7 +90,7 @@ describe "Admin Runners" do
context 'specific runner' do
it 'shows the label and the project count' do
project = create :project
- runner = create :ci_runner, projects: [project]
+ runner = create :ci_runner, :project, projects: [project]
visit admin_runners_path
@@ -149,8 +149,9 @@ describe "Admin Runners" do
end
context 'with specific runner' do
+ let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
+
before do
- @project1.runners << runner
visit admin_runner_path(runner)
end
@@ -158,9 +159,9 @@ describe "Admin Runners" do
end
context 'with locked runner' do
+ let(:runner) { create(:ci_runner, :project, projects: [@project1], locked: true) }
+
before do
- runner.update(locked: true)
- @project1.runners << runner
visit admin_runner_path(runner)
end
@@ -168,9 +169,10 @@ describe "Admin Runners" do
end
context 'with shared runner' do
+ let(:runner) { create(:ci_runner, :instance) }
+
before do
@project1.destroy
- runner.update(is_shared: true)
visit admin_runner_path(runner)
end
@@ -179,8 +181,9 @@ describe "Admin Runners" do
end
describe 'disable/destroy' do
+ let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
+
before do
- @project1.runners << runner
visit admin_runner_path(runner)
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index 193b1dfabbd..32bd7b88840 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -35,7 +35,7 @@ describe 'Issue Boards', :js do
end
it 'moves un-ordered issue to top of list' do
- drag(from_index: 3, to_index: 0)
+ drag(from_index: 3, to_index: 0, duration: 1180)
wait_for_requests
@@ -156,12 +156,13 @@ describe 'Issue Boards', :js do
end
end
- def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1)
+ def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1, duration: 1000)
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
from_index: from_index,
to_index: to_index,
- list_to_index: list_to_index)
+ list_to_index: list_to_index,
+ duration: duration)
end
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index e0cd963fe39..9ce7d538004 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -28,12 +28,8 @@ feature 'Runners' do
project.add_master(user)
end
- context 'when a specific runner is activated on the project' do
- given(:specific_runner) { create(:ci_runner, :specific) }
-
- background do
- project.runners << specific_runner
- end
+ context 'when a project_type runner is activated on the project' do
+ given!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
scenario 'user sees the specific runner' do
visit project_runners_path(project)
@@ -114,7 +110,7 @@ feature 'Runners' do
end
context 'when a shared runner is activated on the project' do
- given!(:shared_runner) { create(:ci_runner, :shared) }
+ given!(:shared_runner) { create(:ci_runner, :instance) }
scenario 'user sees CI/CD setting page' do
visit project_runners_path(project)
@@ -126,11 +122,10 @@ feature 'Runners' do
context 'when a specific runner exists in another project' do
given(:another_project) { create(:project) }
- given(:specific_runner) { create(:ci_runner, :specific) }
+ given!(:specific_runner) { create(:ci_runner, :project, projects: [another_project]) }
background do
another_project.add_master(user)
- another_project.runners << specific_runner
end
scenario 'user enables and disables a specific runner' do
@@ -220,8 +215,8 @@ feature 'Runners' do
end
context 'project with a group but no group runner' do
- given(:group) { create :group }
- given(:project) { create :project, group: group }
+ given(:group) { create(:group) }
+ given(:project) { create(:project, group: group) }
scenario 'group runners are not available' do
visit project_runners_path(project)
@@ -234,9 +229,9 @@ feature 'Runners' do
end
context 'project with a group and a group runner' do
- given(:group) { create :group }
- given(:project) { create :project, group: group }
- given!(:ci_runner) { create :ci_runner, groups: [group], description: 'group-runner' }
+ given(:group) { create(:group) }
+ given(:project) { create(:project, group: group) }
+ given!(:ci_runner) { create(:ci_runner, :group, groups: [group], description: 'group-runner') }
scenario 'group runners are available' do
visit project_runners_path(project)
@@ -263,7 +258,7 @@ feature 'Runners' do
end
context 'group runners in group settings' do
- given(:group) { create :group }
+ given(:group) { create(:group) }
background do
group.add_master(user)
end
@@ -277,7 +272,7 @@ feature 'Runners' do
end
context 'group with a runner' do
- let!(:runner) { create :ci_runner, groups: [group], description: 'group-runner' }
+ let!(:runner) { create(:ci_runner, :group, groups: [group], description: 'group-runner') }
scenario 'the runner is visible' do
visit group_settings_ci_cd_path(group)
diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb
index 4275b1a7ff1..97304170c4e 100644
--- a/spec/finders/runner_jobs_finder_spec.rb
+++ b/spec/finders/runner_jobs_finder_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe RunnerJobsFinder do
let(:project) { create(:project) }
- let(:runner) { create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
subject { described_class.new(runner, params).execute }
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index d27c12e43f2..ccef17a6615 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -31,7 +31,8 @@
}
},
"status_reason": { "type": ["string", "null"] },
- "external_ip": { "type": ["string", "null"] }
+ "external_ip": { "type": ["string", "null"] },
+ "hostname": { "type": ["string", "null"] }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
index 38467b4ca20..00abe73ec8a 100644
--- a/spec/fixtures/api/schemas/entities/issue.json
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -27,7 +27,7 @@
"due_date": { "type": "date" },
"confidential": { "type": "boolean" },
"discussion_locked": { "type": ["boolean", "null"] },
- "updated_by_id": { "type": ["string", "null"] },
+ "updated_by_id": { "type": ["integer", "null"] },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index d56e14e0e0b..c3dfd7bedbe 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -1,5 +1,5 @@
require 'spec_helper'
-require_relative '../../config/initializers/secret_token'
+require_relative '../../config/initializers/01_secret_token'
describe 'create_tokens' do
include StubENV
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index a49b190d36a..de261d36c61 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -3,7 +3,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import Sortable from 'vendor/Sortable';
+import Sortable from 'sortablejs';
import BoardList from '~/boards/components/board_list.vue';
import eventHub from '~/boards/eventhub';
import '~/boards/mixins/sortable_default_options';
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index a5cd247b689..abe2954d506 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -207,11 +207,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
- cluster.installApplication('helm');
+ cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('helm');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -226,11 +226,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
- cluster.installApplication('ingress');
+ cluster.installApplication({ id: 'ingress' });
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -245,11 +245,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
- cluster.installApplication('runner');
+ cluster.installApplication({ id: 'runner' });
expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('runner');
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
getSetTimeoutPromise()
.then(() => {
@@ -260,11 +260,29 @@ describe('Clusters', () => {
.catch(done.fail);
});
+ it('tries to install jupyter', (done) => {
+ spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
+ cluster.installApplication({ id: 'jupyter', params: { hostname: cluster.store.state.applications.jupyter.hostname } });
+
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_LOADING);
+ expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { hostname: cluster.store.state.applications.jupyter.hostname });
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUCCESS);
+ expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
it('sets error request status when the request fails', (done) => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR')));
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
- cluster.installApplication('helm');
+ cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js
index 2c4707bb856..c83cbe90a57 100644
--- a/spec/javascripts/clusters/components/application_row_spec.js
+++ b/spec/javascripts/clusters/components/application_row_spec.js
@@ -174,7 +174,27 @@ describe('Application Row', () => {
installButton.click();
- expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', DEFAULT_APPLICATION_STATE.id);
+ expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
+ id: DEFAULT_APPLICATION_STATE.id,
+ params: {},
+ });
+ });
+
+ it('clicking install button when installApplicationRequestParams are provided emits event', () => {
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_INSTALLABLE,
+ installApplicationRequestParams: { hostname: 'jupyter' },
+ });
+ const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
+
+ installButton.click();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
+ id: DEFAULT_APPLICATION_STATE.id,
+ params: { hostname: 'jupyter' },
+ });
});
it('clicking disabled install button emits nothing', () => {
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index d546543d273..a70138c7eee 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -22,6 +22,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub' },
},
});
});
@@ -41,6 +42,10 @@ describe('Applications', () => {
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
});
+
+ it('renders a row for Jupyter', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
+ });
});
describe('Ingress application', () => {
@@ -57,12 +62,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
- expect(
- vm.$el.querySelector('.js-ip-address').value,
- ).toEqual('0.0.0.0');
+ expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0');
expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
@@ -81,12 +85,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
- expect(
- vm.$el.querySelector('.js-ip-address').value,
- ).toEqual('?');
+ expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?');
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
});
@@ -101,6 +104,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '' },
},
});
@@ -108,5 +112,83 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
});
});
+
+ describe('Jupyter application', () => {
+ describe('with ingress installed with ip & jupyter installable', () => {
+ it('renders hostname active input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual(null);
+ });
+ });
+
+ describe('with ingress installed without external ip', () => {
+ it('does not render hostname input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+ });
+ });
+
+ describe('with ingress & jupyter installed', () => {
+ it('renders readonly input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller', status: 'installed' },
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual('readonly');
+ });
+ });
+
+ describe('without ingress installed', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller' },
+ ingress: { title: 'Ingress' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', status: 'not_installable' },
+ },
+ });
+ });
+
+ it('does not render input', () => {
+ expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+ });
+
+ it('renders disabled install button', () => {
+ expect(
+ vm.$el
+ .querySelector(
+ '.js-cluster-application-row-jupyter .js-cluster-application-install-button',
+ )
+ .getAttribute('disabled'),
+ ).toEqual('disabled');
+ });
+ });
+ });
});
});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 6ae7a792329..b2b0ebf840b 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -1,4 +1,5 @@
import {
+ APPLICATION_INSTALLED,
APPLICATION_INSTALLABLE,
APPLICATION_INSTALLING,
APPLICATION_ERROR,
@@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = {
name: 'prometheus',
status: APPLICATION_ERROR,
status_reason: 'Cannot connect',
+ }, {
+ name: 'jupyter',
+ status: APPLICATION_INSTALLING,
+ status_reason: 'Cannot connect',
+ }],
+ },
+ },
+ '/gitlab-org/gitlab-shell/clusters/2/status.json': {
+ data: {
+ status: 'errored',
+ status_reason: 'Failed to request to CloudPlatform.',
+ applications: [{
+ name: 'helm',
+ status: APPLICATION_INSTALLED,
+ status_reason: null,
+ }, {
+ name: 'ingress',
+ status: APPLICATION_INSTALLED,
+ status_reason: 'Cannot connect',
+ external_ip: '1.1.1.1',
+ }, {
+ name: 'runner',
+ status: APPLICATION_INSTALLING,
+ status_reason: null,
+ },
+ {
+ name: 'prometheus',
+ status: APPLICATION_ERROR,
+ status_reason: 'Cannot connect',
+ }, {
+ name: 'jupyter',
+ status: APPLICATION_INSTALLABLE,
+ status_reason: 'Cannot connect',
}],
},
},
@@ -37,6 +71,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { },
+ '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': { },
},
};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index 8028faf2f02..6854b016852 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -91,8 +91,26 @@ describe('Clusters Store', () => {
requestStatus: null,
requestReason: null,
},
+ jupyter: {
+ title: 'JupyterHub',
+ status: mockResponseData.applications[4].status,
+ statusReason: mockResponseData.applications[4].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ hostname: '',
+ },
},
});
});
+
+ it('sets default hostname for jupyter when ingress has a ip address', () => {
+ const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
+
+ store.updateStateFromServer(mockResponseData);
+
+ expect(
+ store.state.applications.jupyter.hostname,
+ ).toEqual(`jupyter.${store.state.applications.ingress.externalIp}.xip.io`);
+ });
});
});
diff --git a/spec/javascripts/ide/components/jobs/item_spec.js b/spec/javascripts/ide/components/jobs/item_spec.js
new file mode 100644
index 00000000000..7c1dd4e475c
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/item_spec.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import JobItem from '~/ide/components/jobs/item.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+import { jobs } from '../../mock_data';
+
+describe('IDE jobs item', () => {
+ const Component = Vue.extend(JobItem);
+ const job = jobs[0];
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ job,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders job details', () => {
+ expect(vm.$el.textContent).toContain(job.name);
+ expect(vm.$el.textContent).toContain(`#${job.id}`);
+ });
+
+ it('renders CI icon', () => {
+ expect(vm.$el.querySelector('.ic-status_passed_borderless')).not.toBe(null);
+ });
+});
diff --git a/spec/javascripts/ide/components/jobs/list_spec.js b/spec/javascripts/ide/components/jobs/list_spec.js
new file mode 100644
index 00000000000..b24853c56fa
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/list_spec.js
@@ -0,0 +1,67 @@
+import Vue from 'vue';
+import StageList from '~/ide/components/jobs/list.vue';
+import { createStore } from '~/ide/stores';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { stages, jobs } from '../../mock_data';
+
+describe('IDE stages list', () => {
+ const Component = Vue.extend(StageList);
+ let vm;
+
+ beforeEach(() => {
+ const store = createStore();
+
+ vm = createComponentWithStore(Component, store, {
+ stages: stages.map((mappedState, i) => ({
+ ...mappedState,
+ id: i,
+ dropdownPath: mappedState.dropdown_path,
+ jobs: [...jobs],
+ isLoading: false,
+ isCollapsed: false,
+ })),
+ loading: false,
+ });
+
+ spyOn(vm, 'fetchJobs');
+ spyOn(vm, 'toggleStageCollapsed');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders list of stages', () => {
+ expect(vm.$el.querySelectorAll('.card').length).toBe(2);
+ });
+
+ it('renders loading icon when no stages & is loading', done => {
+ vm.stages = [];
+ vm.loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('calls toggleStageCollapsed when clicking stage header', done => {
+ vm.$el.querySelector('.card-header').click();
+
+ vm.$nextTick(() => {
+ expect(vm.toggleStageCollapsed).toHaveBeenCalledWith(0);
+
+ done();
+ });
+ });
+
+ it('calls fetchJobs when stage is mounted', () => {
+ expect(vm.fetchJobs.calls.count()).toBe(stages.length);
+
+ expect(vm.fetchJobs.calls.argsFor(0)).toEqual([vm.stages[0]]);
+ expect(vm.fetchJobs.calls.argsFor(1)).toEqual([vm.stages[1]]);
+ });
+});
diff --git a/spec/javascripts/ide/components/jobs/stage_spec.js b/spec/javascripts/ide/components/jobs/stage_spec.js
new file mode 100644
index 00000000000..fc3831f2d05
--- /dev/null
+++ b/spec/javascripts/ide/components/jobs/stage_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import Stage from '~/ide/components/jobs/stage.vue';
+import { stages, jobs } from '../../mock_data';
+
+describe('IDE pipeline stage', () => {
+ const Component = Vue.extend(Stage);
+ let vm;
+ let stage;
+
+ beforeEach(() => {
+ stage = {
+ ...stages[0],
+ id: 0,
+ dropdownPath: stages[0].dropdown_path,
+ jobs: [...jobs],
+ isLoading: false,
+ isCollapsed: false,
+ };
+
+ vm = new Component({
+ propsData: { stage },
+ });
+
+ spyOn(vm, '$emit');
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('emits fetch event when mounted', () => {
+ expect(vm.$emit).toHaveBeenCalledWith('fetch', vm.stage);
+ });
+
+ it('renders stages details', () => {
+ expect(vm.$el.textContent).toContain(vm.stage.name);
+ });
+
+ it('renders CI icon', () => {
+ expect(vm.$el.querySelector('.ic-status_failed')).not.toBe(null);
+ });
+
+ describe('collapsed', () => {
+ it('emits event when clicking header', done => {
+ vm.$el.querySelector('.card-header').click();
+
+ vm.$nextTick(() => {
+ expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed', vm.stage.id);
+
+ done();
+ });
+ });
+
+ it('toggles collapse status when collapsed', done => {
+ vm.stage.isCollapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.card-body').style.display).toBe('none');
+
+ done();
+ });
+ });
+
+ it('sets border bottom class when collapsed', done => {
+ vm.stage.isCollapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.card-header').classList).toContain('border-bottom-0');
+
+ done();
+ });
+ });
+ });
+
+ it('renders jobs count', () => {
+ expect(vm.$el.querySelector('.badge').textContent).toContain('4');
+ });
+
+ it('renders loading icon when no jobs and isLoading is true', done => {
+ vm.stage.isLoading = true;
+ vm.stage.jobs = [];
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders list of jobs', () => {
+ expect(vm.$el.querySelectorAll('.ide-job-item').length).toBe(4);
+ });
+});
diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js
new file mode 100644
index 00000000000..2bb5aa08c3b
--- /dev/null
+++ b/spec/javascripts/ide/components/pipelines/list_spec.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { createStore } from '~/ide/stores';
+import List from '~/ide/components/pipelines/list.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+import { pipelines, projectData, stages, jobs } from '../../mock_data';
+
+describe('IDE pipelines list', () => {
+ const Component = Vue.extend(List);
+ let vm;
+ let mock;
+
+ beforeEach(done => {
+ const store = createStore();
+
+ mock = new MockAdapter(axios);
+
+ store.state.currentProjectId = 'abc/def';
+ store.state.currentBranchId = 'master';
+ store.state.projects['abc/def'] = {
+ ...projectData,
+ path_with_namespace: 'abc/def',
+ branches: {
+ master: { commit: { id: '123' } },
+ },
+ };
+ store.state.links = { ciHelpPagePath: gl.TEST_HOST };
+ store.state.pipelinesEmptyStateSvgPath = gl.TEST_HOST;
+ store.state.pipelines.stages = stages.map((mappedState, i) => ({
+ ...mappedState,
+ id: i,
+ dropdownPath: mappedState.dropdown_path,
+ jobs: [...jobs],
+ isLoading: false,
+ isCollapsed: false,
+ }));
+
+ mock
+ .onGet('/abc/def/commit/123/pipelines')
+ .replyOnce(200, { pipelines: [...pipelines] }, { 'poll-interval': '-1' });
+
+ vm = createComponentWithStore(Component, store).$mount();
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ vm.$store.dispatch('pipelines/stopPipelinePolling');
+ vm.$store.dispatch('pipelines/clearEtagPoll');
+
+ vm.$destroy();
+ mock.restore();
+ });
+
+ it('renders pipeline data', () => {
+ expect(vm.$el.textContent).toContain('#1');
+ });
+
+ it('renders CI icon', () => {
+ expect(vm.$el.querySelector('.ci-status-icon-failed')).not.toBe(null);
+ });
+
+ it('renders list of jobs', () => {
+ expect(vm.$el.querySelectorAll('.tab-pane:first-child .ide-job-item').length).toBe(
+ jobs.length * stages.length,
+ );
+ });
+
+ it('renders list of failed jobs on failed jobs tab', done => {
+ vm.$el.querySelectorAll('.tab-links a')[1].click();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.tab-pane.active .ide-job-item').length).toBe(2);
+
+ done();
+ });
+ });
+
+ describe('YAML error', () => {
+ it('renders YAML error', done => {
+ vm.$store.state.pipelines.latestPipeline.yamlError = 'test yaml error';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).toContain('Found errors in your .gitlab-ci.yml:');
+ expect(vm.$el.textContent).toContain('test yaml error');
+
+ done();
+ });
+ });
+ });
+
+ describe('empty state', () => {
+ it('renders pipelines empty state', done => {
+ vm.$store.state.pipelines.latestPipeline = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.empty-state')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ describe('loading state', () => {
+ it('renders loading state when there is no latest pipeline', done => {
+ vm.$store.state.pipelines.latestPipeline = null;
+ vm.$store.state.pipelines.isLoadingPipeline = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js
index 98db6defc7a..9312e17704e 100644
--- a/spec/javascripts/ide/helpers.js
+++ b/spec/javascripts/ide/helpers.js
@@ -1,11 +1,15 @@
import { decorateData } from '~/ide/stores/utils';
import state from '~/ide/stores/state';
import commitState from '~/ide/stores/modules/commit/state';
+import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
+import pipelinesState from '~/ide/stores/modules/pipelines/state';
export const resetStore = store => {
const newState = {
...state(),
commit: commitState(),
+ mergeRequests: mergeRequestsState(),
+ pipelines: pipelinesState(),
};
store.replaceState(newState);
};
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
index c68ae050641..dcf857f7e04 100644
--- a/spec/javascripts/ide/mock_data.js
+++ b/spec/javascripts/ide/mock_data.js
@@ -19,13 +19,48 @@ export const pipelines = [
id: 1,
ref: 'master',
sha: '123',
- status: 'failed',
+ details: {
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'Failed',
+ },
+ },
+ commit: { id: '123' },
},
{
id: 2,
ref: 'master',
sha: '213',
- status: 'success',
+ details: {
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ text: 'Failed',
+ },
+ },
+ commit: { id: '213' },
+ },
+];
+
+export const stages = [
+ {
+ dropdown_path: `${gl.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',
+ },
},
];
@@ -33,28 +68,44 @@ export const jobs = [
{
id: 1,
name: 'test',
- status: 'failed',
+ path: 'testing',
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
stage: 'test',
duration: 1,
},
{
id: 2,
name: 'test 2',
- status: 'failed',
+ path: 'testing2',
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
stage: 'test',
duration: 1,
},
{
id: 3,
name: 'test 3',
- status: 'failed',
+ path: 'testing3',
+ status: {
+ icon: 'status_passed',
+ text: 'passed',
+ },
stage: 'test',
duration: 1,
},
{
id: 4,
- name: 'test 3',
- status: 'failed',
+ name: 'test 4',
+ path: 'testing4',
+ status: {
+ icon: 'status_failed',
+ text: 'failed',
+ },
stage: 'build',
duration: 1,
},
@@ -68,14 +119,16 @@ export const fullPipelinesResponse = {
pipelines: [
{
id: '51',
+ path: 'test',
commit: {
- id: 'xxxxxxxxxxxxxxxxxxxx',
+ id: '123',
},
details: {
status: {
icon: 'status_failed',
text: 'failed',
},
+ stages: [...stages],
},
},
{
@@ -88,8 +141,19 @@ export const fullPipelinesResponse = {
icon: 'status_passed',
text: 'passed',
},
+ stages: [...stages],
},
},
],
},
};
+
+export const mergeRequests = [
+ {
+ id: 1,
+ iid: 1,
+ title: 'Test merge request',
+ project_id: 1,
+ web_url: `${gl.TEST_HOST}/namespace/project-path/merge_requests/1`,
+ },
+];
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index 8e078ae7138..d71fc0e035e 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -1,31 +1,10 @@
-import Visibility from 'visibilityjs';
-import MockAdapter from 'axios-mock-adapter';
-import { refreshLastCommitData, pollSuccessCallBack } from '~/ide/stores/actions';
+import { refreshLastCommitData } from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
-import axios from '~/lib/utils/axios_utils';
-import { fullPipelinesResponse } from '../../mock_data';
import { resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store project actions', () => {
- const setProjectState = () => {
- store.state.currentProjectId = 'abc/def';
- store.state.currentBranchId = 'master';
- store.state.projects['abc/def'] = {
- id: 4,
- path_with_namespace: 'abc/def',
- branches: {
- master: {
- commit: {
- id: 'abc123def456ghi789jkl',
- title: 'example',
- },
- },
- },
- };
- };
-
beforeEach(() => {
store.state.projects['abc/def'] = {};
});
@@ -101,92 +80,4 @@ describe('IDE store project actions', () => {
);
});
});
-
- describe('pipelinePoll', () => {
- let mock;
-
- beforeEach(() => {
- setProjectState();
- jasmine.clock().install();
- mock = new MockAdapter(axios);
- mock
- .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
- .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- mock.restore();
- store.dispatch('stopPipelinePolling');
- });
-
- it('calls service periodically', done => {
- spyOn(axios, 'get').and.callThrough();
- spyOn(Visibility, 'hidden').and.returnValue(false);
-
- store
- .dispatch('pipelinePoll')
- .then(() => {
- jasmine.clock().tick(1000);
-
- expect(axios.get).toHaveBeenCalled();
- expect(axios.get.calls.count()).toBe(1);
- })
- .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
- .then(() => {
- jasmine.clock().tick(10000);
- expect(axios.get.calls.count()).toBe(2);
- })
- .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
- .then(() => {
- jasmine.clock().tick(10000);
- expect(axios.get.calls.count()).toBe(3);
- })
- .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
- .then(() => {
- jasmine.clock().tick(10000);
- expect(axios.get.calls.count()).toBe(4);
- })
-
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('pollSuccessCallBack', () => {
- beforeEach(() => {
- setProjectState();
- });
-
- it('commits correct pipeline', done => {
- testAction(
- pollSuccessCallBack,
- fullPipelinesResponse,
- store.state,
- [
- {
- type: 'SET_LAST_COMMIT_PIPELINE',
- payload: {
- projectId: 'abc/def',
- branchId: 'master',
- pipeline: {
- id: '50',
- commit: {
- id: 'abc123def456ghi789jkl',
- },
- details: {
- status: {
- icon: 'status_passed',
- text: 'passed',
- },
- },
- },
- },
- },
- ], // mutations
- [], // action
- done,
- );
- });
- });
});
diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
new file mode 100644
index 00000000000..b571cfb963a
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js
@@ -0,0 +1,182 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import state from '~/ide/stores/modules/merge_requests/state';
+import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
+import actions, {
+ requestMergeRequests,
+ receiveMergeRequestsError,
+ receiveMergeRequestsSuccess,
+ fetchMergeRequests,
+ resetMergeRequests,
+} from '~/ide/stores/modules/merge_requests/actions';
+import { mergeRequests } from '../../../mock_data';
+import testAction from '../../../../helpers/vuex_action_helper';
+
+describe('IDE merge requests actions', () => {
+ let mockedState;
+ let mock;
+
+ beforeEach(() => {
+ mockedState = state();
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('requestMergeRequests', () => {
+ it('should should commit request', done => {
+ testAction(
+ requestMergeRequests,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_MERGE_REQUESTS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveMergeRequestsError', () => {
+ let flashSpy;
+
+ beforeEach(() => {
+ flashSpy = spyOnDependency(actions, 'flash');
+ });
+
+ it('should should commit error', done => {
+ testAction(
+ receiveMergeRequestsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }],
+ [],
+ done,
+ );
+ });
+
+ it('creates flash message', () => {
+ receiveMergeRequestsError({ commit() {} });
+
+ expect(flashSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('receiveMergeRequestsSuccess', () => {
+ it('should commit received data', done => {
+ testAction(
+ receiveMergeRequestsSuccess,
+ 'data',
+ mockedState,
+ [{ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, payload: 'data' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchMergeRequests', () => {
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests);
+ });
+
+ it('calls API with params from state', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchMergeRequests({ dispatch() {}, state: mockedState });
+
+ expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
+ params: {
+ scope: 'assigned-to-me',
+ state: 'opened',
+ search: '',
+ },
+ });
+ });
+
+ it('calls API with search', () => {
+ const apiSpy = spyOn(axios, 'get').and.callThrough();
+
+ fetchMergeRequests({ dispatch() {}, state: mockedState }, 'testing search');
+
+ expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
+ params: {
+ scope: 'assigned-to-me',
+ state: 'opened',
+ search: 'testing search',
+ },
+ });
+ });
+
+ it('dispatches request', done => {
+ testAction(
+ fetchMergeRequests,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestMergeRequests' },
+ { type: 'resetMergeRequests' },
+ { type: 'receiveMergeRequestsSuccess' },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches success with received data', done => {
+ testAction(
+ fetchMergeRequests,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestMergeRequests' },
+ { type: 'resetMergeRequests' },
+ { type: 'receiveMergeRequestsSuccess', payload: mergeRequests },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500);
+ });
+
+ it('dispatches error', done => {
+ testAction(
+ fetchMergeRequests,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestMergeRequests' },
+ { type: 'resetMergeRequests' },
+ { type: 'receiveMergeRequestsError' },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('resetMergeRequests', () => {
+ it('commits reset', done => {
+ testAction(
+ resetMergeRequests,
+ null,
+ mockedState,
+ [{ type: types.RESET_MERGE_REQUESTS }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js
new file mode 100644
index 00000000000..664d3914564
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js
@@ -0,0 +1,55 @@
+import state from '~/ide/stores/modules/merge_requests/state';
+import mutations from '~/ide/stores/modules/merge_requests/mutations';
+import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
+import { mergeRequests } from '../../../mock_data';
+
+describe('IDE merge requests mutations', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe(types.REQUEST_MERGE_REQUESTS, () => {
+ it('sets loading to true', () => {
+ mutations[types.REQUEST_MERGE_REQUESTS](mockedState);
+
+ expect(mockedState.isLoading).toBe(true);
+ });
+ });
+
+ describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => {
+ it('sets loading to false', () => {
+ mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState);
+
+ expect(mockedState.isLoading).toBe(false);
+ });
+ });
+
+ describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => {
+ it('sets merge requests', () => {
+ gon.gitlab_url = gl.TEST_HOST;
+ mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, mergeRequests);
+
+ expect(mockedState.mergeRequests).toEqual([
+ {
+ id: 1,
+ iid: 1,
+ title: 'Test merge request',
+ projectId: 1,
+ projectPathWithNamespace: 'namespace/project-path',
+ },
+ ]);
+ });
+ });
+
+ describe(types.RESET_MERGE_REQUESTS, () => {
+ it('clears merge request array', () => {
+ mockedState.mergeRequests = ['test'];
+
+ mutations[types.RESET_MERGE_REQUESTS](mockedState);
+
+ expect(mockedState.mergeRequests).toEqual([]);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
index 85fbcf8084b..f26eaf9c81f 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js
@@ -1,3 +1,4 @@
+import Visibility from 'visibilityjs';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import actions, {
@@ -5,10 +6,13 @@ import actions, {
receiveLatestPipelineError,
receiveLatestPipelineSuccess,
fetchLatestPipeline,
+ stopPipelinePolling,
+ clearEtagPoll,
requestJobs,
receiveJobsError,
receiveJobsSuccess,
fetchJobs,
+ toggleStageCollapsed,
} from '~/ide/stores/modules/pipelines/actions';
import state from '~/ide/stores/modules/pipelines/state';
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
@@ -51,7 +55,7 @@ describe('IDE pipelines actions', () => {
null,
mockedState,
[{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
- [],
+ [{ type: 'stopPipelinePolling' }],
done,
);
});
@@ -59,91 +63,128 @@ describe('IDE pipelines actions', () => {
it('creates flash message', () => {
const flashSpy = spyOnDependency(actions, 'flash');
- receiveLatestPipelineError({ commit() {} });
+ receiveLatestPipelineError({ commit() {}, dispatch() {} });
expect(flashSpy).toHaveBeenCalled();
});
});
describe('receiveLatestPipelineSuccess', () => {
- it('commits pipeline', done => {
- testAction(
- receiveLatestPipelineSuccess,
+ const rootGetters = {
+ lastCommit: { id: '123' },
+ };
+ let commit;
+
+ beforeEach(() => {
+ commit = jasmine.createSpy('commit');
+ });
+
+ it('commits pipeline', () => {
+ receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines });
+
+ expect(commit.calls.argsFor(0)).toEqual([
+ types.RECEIVE_LASTEST_PIPELINE_SUCCESS,
pipelines[0],
- mockedState,
- [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: pipelines[0] }],
- [],
- done,
- );
+ ]);
+ });
+
+ it('commits false when there are no pipelines', () => {
+ receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines: [] });
+
+ expect(commit.calls.argsFor(0)).toEqual([types.RECEIVE_LASTEST_PIPELINE_SUCCESS, false]);
});
});
describe('fetchLatestPipeline', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ stopPipelinePolling();
+ clearEtagPoll();
+ });
+
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(200, pipelines);
+ mock
+ .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
+ .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
});
it('dispatches request', done => {
- testAction(
- fetchLatestPipeline,
- '123',
- mockedState,
- [],
- [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineSuccess' }],
- done,
- );
- });
-
- it('dispatches success with latest pipeline', done => {
- testAction(
- fetchLatestPipeline,
- '123',
- mockedState,
- [],
- [
- { type: 'requestLatestPipeline' },
- { type: 'receiveLatestPipelineSuccess', payload: pipelines[0] },
- ],
- done,
- );
- });
-
- it('calls axios with correct params', () => {
- const apiSpy = spyOn(axios, 'get').and.callThrough();
-
- fetchLatestPipeline({ dispatch() {}, rootState: state }, '123');
-
- expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
- params: {
- sha: '123',
- per_page: '1',
- },
- });
+ spyOn(axios, 'get').and.callThrough();
+ spyOn(Visibility, 'hidden').and.returnValue(false);
+
+ const dispatch = jasmine.createSpy('dispatch');
+ const rootGetters = {
+ lastCommit: { id: 'abc123def456ghi789jkl' },
+ currentProject: { path_with_namespace: 'abc/def' },
+ };
+
+ fetchLatestPipeline({ dispatch, rootGetters });
+
+ expect(dispatch.calls.argsFor(0)).toEqual(['requestLatestPipeline']);
+
+ jasmine.clock().tick(1000);
+
+ new Promise(resolve => requestAnimationFrame(resolve))
+ .then(() => {
+ expect(axios.get).toHaveBeenCalled();
+ expect(axios.get.calls.count()).toBe(1);
+
+ expect(dispatch.calls.argsFor(1)).toEqual([
+ 'receiveLatestPipelineSuccess',
+ jasmine.anything(),
+ ]);
+
+ jasmine.clock().tick(10000);
+ })
+ .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
+ .then(() => {
+ expect(axios.get).toHaveBeenCalled();
+ expect(axios.get.calls.count()).toBe(2);
+
+ expect(dispatch.calls.argsFor(2)).toEqual([
+ 'receiveLatestPipelineSuccess',
+ jasmine.anything(),
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
describe('error', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
+ mock.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines').reply(500);
});
it('dispatches error', done => {
- testAction(
- fetchLatestPipeline,
- '123',
- mockedState,
- [],
- [{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineError' }],
- done,
- );
+ const dispatch = jasmine.createSpy('dispatch');
+ const rootGetters = {
+ lastCommit: { id: 'abc123def456ghi789jkl' },
+ currentProject: { path_with_namespace: 'abc/def' },
+ };
+
+ fetchLatestPipeline({ dispatch, rootGetters });
+
+ jasmine.clock().tick(1500);
+
+ new Promise(resolve => requestAnimationFrame(resolve))
+ .then(() => {
+ expect(dispatch.calls.argsFor(1)).toEqual(['receiveLatestPipelineError']);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
describe('requestJobs', () => {
it('commits request', done => {
- testAction(requestJobs, null, mockedState, [{ type: types.REQUEST_JOBS }], [], done);
+ testAction(requestJobs, 1, mockedState, [{ type: types.REQUEST_JOBS, payload: 1 }], [], done);
});
});
@@ -151,9 +192,9 @@ describe('IDE pipelines actions', () => {
it('commits error', done => {
testAction(
receiveJobsError,
- null,
+ 1,
mockedState,
- [{ type: types.RECEIVE_JOBS_ERROR }],
+ [{ type: types.RECEIVE_JOBS_ERROR, payload: 1 }],
[],
done,
);
@@ -162,19 +203,19 @@ describe('IDE pipelines actions', () => {
it('creates flash message', () => {
const flashSpy = spyOnDependency(actions, 'flash');
- receiveJobsError({ commit() {} });
+ receiveJobsError({ commit() {} }, 1);
expect(flashSpy).toHaveBeenCalled();
});
});
describe('receiveJobsSuccess', () => {
- it('commits jobs', done => {
+ it('commits data', done => {
testAction(
receiveJobsSuccess,
- jobs,
+ { id: 1, data: jobs },
mockedState,
- [{ type: types.RECEIVE_JOBS_SUCCESS, payload: jobs }],
+ [{ type: types.RECEIVE_JOBS_SUCCESS, payload: { id: 1, data: jobs } }],
[],
done,
);
@@ -182,108 +223,62 @@ describe('IDE pipelines actions', () => {
});
describe('fetchJobs', () => {
- let page = '';
-
- beforeEach(() => {
- mockedState.latestPipeline = pipelines[0];
- });
+ const stage = {
+ id: 1,
+ dropdownPath: `${gl.TEST_HOST}/jobs`,
+ };
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines\/(.*)\/jobs/).replyOnce(() => [
- 200,
- jobs,
- {
- 'x-next-page': page,
- },
- ]);
+ mock.onGet(stage.dropdownPath).replyOnce(200, jobs);
});
it('dispatches request', done => {
testAction(
fetchJobs,
- null,
- mockedState,
- [],
- [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess' }],
- done,
- );
- });
-
- it('dispatches success with latest pipeline', done => {
- testAction(
- fetchJobs,
- null,
- mockedState,
- [],
- [{ type: 'requestJobs' }, { type: 'receiveJobsSuccess', payload: jobs }],
- done,
- );
- });
-
- it('dispatches twice for both pages', done => {
- page = '2';
-
- testAction(
- fetchJobs,
- null,
+ stage,
mockedState,
[],
[
- { type: 'requestJobs' },
- { type: 'receiveJobsSuccess', payload: jobs },
- { type: 'fetchJobs', payload: '2' },
- { type: 'requestJobs' },
- { type: 'receiveJobsSuccess', payload: jobs },
+ { type: 'requestJobs', payload: stage.id },
+ { type: 'receiveJobsSuccess', payload: { id: stage.id, data: jobs } },
],
done,
);
});
-
- it('calls axios with correct URL', () => {
- const apiSpy = spyOn(axios, 'get').and.callThrough();
-
- fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
-
- expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
- params: { page: '1' },
- });
- });
-
- it('calls axios with page next page', () => {
- const apiSpy = spyOn(axios, 'get').and.callThrough();
-
- fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
-
- expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
- params: { page: '1' },
- });
-
- page = '2';
-
- fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }, page);
-
- expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
- params: { page: '2' },
- });
- });
});
describe('error', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
+ mock.onGet(stage.dropdownPath).replyOnce(500);
});
it('dispatches error', done => {
testAction(
fetchJobs,
- null,
+ stage,
mockedState,
[],
- [{ type: 'requestJobs' }, { type: 'receiveJobsError' }],
+ [
+ { type: 'requestJobs', payload: stage.id },
+ { type: 'receiveJobsError', payload: stage.id },
+ ],
done,
);
});
});
});
+
+ describe('toggleStageCollapsed', () => {
+ it('commits collapse', done => {
+ testAction(
+ toggleStageCollapsed,
+ 1,
+ mockedState,
+ [{ type: types.TOGGLE_STAGE_COLLAPSE, payload: 1 }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
index b2a7e8a9025..4514896b5ea 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
@@ -37,35 +37,4 @@ describe('IDE pipeline getters', () => {
expect(getters.hasLatestPipeline(mockedState)).toBe(true);
});
});
-
- describe('failedJobs', () => {
- it('returns array of failed jobs', () => {
- mockedState.stages = [
- {
- title: 'test',
- jobs: [{ id: 1, status: 'failed' }, { id: 2, status: 'success' }],
- },
- {
- title: 'build',
- jobs: [{ id: 3, status: 'failed' }, { id: 4, status: 'failed' }],
- },
- ];
-
- expect(getters.failedJobs(mockedState).length).toBe(3);
- expect(getters.failedJobs(mockedState)).toEqual([
- {
- id: 1,
- status: jasmine.anything(),
- },
- {
- id: 3,
- status: jasmine.anything(),
- },
- {
- id: 4,
- status: jasmine.anything(),
- },
- ]);
- });
- });
});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
index 8262e916243..6285c01d483 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
@@ -1,7 +1,7 @@
import mutations from '~/ide/stores/modules/pipelines/mutations';
import state from '~/ide/stores/modules/pipelines/state';
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
-import { pipelines, jobs } from '../../../mock_data';
+import { fullPipelinesResponse, stages, jobs } from '../../../mock_data';
describe('IDE pipelines mutations', () => {
let mockedState;
@@ -28,93 +28,147 @@ describe('IDE pipelines mutations', () => {
describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
it('sets loading to false on success', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
+ mockedState,
+ fullPipelinesResponse.data.pipelines[0],
+ );
expect(mockedState.isLoadingPipeline).toBe(false);
});
it('sets latestPipeline', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
+ mockedState,
+ fullPipelinesResponse.data.pipelines[0],
+ );
expect(mockedState.latestPipeline).toEqual({
- id: pipelines[0].id,
- status: pipelines[0].status,
+ id: '51',
+ path: 'test',
+ commit: { id: '123' },
+ details: { status: jasmine.any(Object) },
+ yamlError: undefined,
});
});
it('does not set latest pipeline if pipeline is null', () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
- expect(mockedState.latestPipeline).toEqual(null);
+ expect(mockedState.latestPipeline).toEqual(false);
+ });
+
+ it('sets stages', () => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
+ mockedState,
+ fullPipelinesResponse.data.pipelines[0],
+ );
+
+ expect(mockedState.stages.length).toBe(2);
+ expect(mockedState.stages).toEqual([
+ {
+ id: 0,
+ dropdownPath: stages[0].dropdown_path,
+ name: stages[0].name,
+ status: stages[0].status,
+ isCollapsed: false,
+ isLoading: false,
+ jobs: [],
+ },
+ {
+ id: 1,
+ dropdownPath: stages[1].dropdown_path,
+ name: stages[1].name,
+ status: stages[1].status,
+ isCollapsed: false,
+ isLoading: false,
+ jobs: [],
+ },
+ ]);
});
});
describe(types.REQUEST_JOBS, () => {
- it('sets jobs loading to true', () => {
- mutations[types.REQUEST_JOBS](mockedState);
+ beforeEach(() => {
+ mockedState.stages = stages.map((stage, i) => ({
+ ...stage,
+ id: i,
+ }));
+ });
+
+ it('sets isLoading on stage', () => {
+ mutations[types.REQUEST_JOBS](mockedState, mockedState.stages[0].id);
- expect(mockedState.isLoadingJobs).toBe(true);
+ expect(mockedState.stages[0].isLoading).toBe(true);
});
});
describe(types.RECEIVE_JOBS_ERROR, () => {
- it('sets jobs loading to false', () => {
- mutations[types.RECEIVE_JOBS_ERROR](mockedState);
+ beforeEach(() => {
+ mockedState.stages = stages.map((stage, i) => ({
+ ...stage,
+ id: i,
+ }));
+ });
+
+ it('sets isLoading on stage after error', () => {
+ mutations[types.RECEIVE_JOBS_ERROR](mockedState, mockedState.stages[0].id);
- expect(mockedState.isLoadingJobs).toBe(false);
+ expect(mockedState.stages[0].isLoading).toBe(false);
});
});
describe(types.RECEIVE_JOBS_SUCCESS, () => {
- it('sets jobs loading to false on success', () => {
- mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+ let data;
- expect(mockedState.isLoadingJobs).toBe(false);
+ beforeEach(() => {
+ mockedState.stages = stages.map((stage, i) => ({
+ ...stage,
+ id: i,
+ }));
+
+ data = {
+ latest_statuses: [...jobs],
+ };
});
- it('sets stages', () => {
- mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+ it('updates loading', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data });
- expect(mockedState.stages.length).toBe(2);
- expect(mockedState.stages).toEqual([
- {
- title: 'test',
- jobs: jasmine.anything(),
- },
- {
- title: 'build',
- jobs: jasmine.anything(),
- },
- ]);
+ expect(mockedState.stages[0].isLoading).toBe(false);
});
- it('sets jobs in stages', () => {
- mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
+ it('sets jobs on stage', () => {
+ mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data });
+
+ expect(mockedState.stages[0].jobs.length).toBe(jobs.length);
+ expect(mockedState.stages[0].jobs).toEqual(
+ jobs.map(job => ({
+ id: job.id,
+ name: job.name,
+ status: job.status,
+ path: job.build_path,
+ })),
+ );
+ });
+ });
- expect(mockedState.stages[0].jobs.length).toBe(3);
- expect(mockedState.stages[1].jobs.length).toBe(1);
- expect(mockedState.stages).toEqual([
- {
- title: jasmine.anything(),
- jobs: jobs.filter(job => job.stage === 'test').map(job => ({
- id: job.id,
- name: job.name,
- status: job.status,
- stage: job.stage,
- duration: job.duration,
- })),
- },
- {
- title: jasmine.anything(),
- jobs: jobs.filter(job => job.stage === 'build').map(job => ({
- id: job.id,
- name: job.name,
- status: job.status,
- stage: job.stage,
- duration: job.duration,
- })),
- },
- ]);
+ describe(types.TOGGLE_STAGE_COLLAPSE, () => {
+ beforeEach(() => {
+ mockedState.stages = stages.map((stage, i) => ({
+ ...stage,
+ id: i,
+ isCollapsed: false,
+ }));
+ });
+
+ it('toggles collapsed state', () => {
+ mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id);
+
+ expect(mockedState.stages[0].isCollapsed).toBe(true);
+
+ mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id);
+
+ expect(mockedState.stages[0].isCollapsed).toBe(false);
});
});
});
diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js
index f2f1f2a9a2e..29eb859ddaf 100644
--- a/spec/javascripts/ide/stores/mutations/branch_spec.js
+++ b/spec/javascripts/ide/stores/mutations/branch_spec.js
@@ -37,40 +37,4 @@ describe('Multi-file store branch mutations', () => {
expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
});
});
-
- describe('SET_LAST_COMMIT_PIPELINE', () => {
- it('sets the pipeline for the last commit on current project', () => {
- localState.projects = {
- Example: {
- branches: {
- master: {
- commit: {},
- },
- },
- },
- };
-
- mutations.SET_LAST_COMMIT_PIPELINE(localState, {
- projectId: 'Example',
- branchId: 'master',
- pipeline: {
- id: '50',
- details: {
- status: {
- icon: 'status_passed',
- text: 'passed',
- },
- },
- },
- });
-
- expect(localState.projects.Example.branches.master.commit.pipeline.id).toBe('50');
- expect(localState.projects.Example.branches.master.commit.pipeline.details.status.text).toBe(
- 'passed',
- );
- expect(localState.projects.Example.branches.master.commit.pipeline.details.status.icon).toBe(
- 'status_passed',
- );
- });
- });
});
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
index 050b1f2074e..e07343810d2 100644
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -143,6 +143,7 @@ describe('IntegrationSettingsForm', () => {
error: true,
message: errorMessage,
service_response: 'some error',
+ test_failed: true,
});
integrationSettingsForm.testSettings(formData)
@@ -157,6 +158,27 @@ describe('IntegrationSettingsForm', () => {
.catch(done.fail);
});
+ it('should not show error Flash with `Save anyway` action if ajax request responds with error in validation', (done) => {
+ const errorMessage = 'Validations failed.';
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ service_response: 'some error',
+ test_failed: false,
+ });
+
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ const $flashContainer = $('.flash-container');
+ expect($flashContainer.find('.flash-text').text().trim()).toEqual('Validations failed. some error');
+ expect($flashContainer.find('.flash-action')).toBeDefined();
+ expect($flashContainer.find('.flash-action').text().trim()).toEqual('');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
it('should submit form if ajax request responds without any error in test', (done) => {
spyOn(integrationSettingsForm.$form, 'submit');
@@ -180,6 +202,7 @@ describe('IntegrationSettingsForm', () => {
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: errorMessage,
+ test_failed: true,
});
integrationSettingsForm.testSettings(formData)
diff --git a/spec/javascripts/vue_shared/components/tabs/tab_spec.js b/spec/javascripts/vue_shared/components/tabs/tab_spec.js
new file mode 100644
index 00000000000..8437fe37738
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/tabs/tab_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tab component', () => {
+ const Component = Vue.extend(Tab);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component);
+ });
+
+ it('sets localActive to equal active', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.localActive).toBe(true);
+
+ done();
+ });
+ });
+
+ it('sets active class', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).toContain('active');
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
new file mode 100644
index 00000000000..50ba18cd338
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import Tabs from '~/vue_shared/components/tabs/tabs';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tabs component', () => {
+ let vm;
+
+ beforeEach(done => {
+ vm = new Vue({
+ components: {
+ Tabs,
+ Tab,
+ },
+ template: `
+ <div>
+ <tabs>
+ <tab title="Testing" active>
+ First tab
+ </tab>
+ <tab>
+ <template slot="title">Test slot</template>
+ Second tab
+ </tab>
+ </tabs>
+ </div>
+ `,
+ }).$mount();
+
+ setTimeout(done);
+ });
+
+ describe('tab links', () => {
+ it('renders links for tabs', () => {
+ expect(vm.$el.querySelectorAll('a').length).toBe(2);
+ });
+
+ it('renders link titles from props', () => {
+ expect(vm.$el.querySelector('a').textContent).toContain('Testing');
+ });
+
+ it('renders link titles from slot', () => {
+ expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
+ });
+
+ it('renders active class', () => {
+ expect(vm.$el.querySelector('a').classList).toContain('active');
+ });
+
+ it('updates active class on click', done => {
+ vm.$el.querySelectorAll('a')[1].click();
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('a').classList).not.toContain('active');
+ expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active');
+
+ done();
+ });
+ });
+ });
+
+ describe('content', () => {
+ it('renders content panes', () => {
+ expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
+ expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
+ expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
+ });
+ });
+});
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index 023bedaaebb..f583b2021a2 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -92,7 +92,7 @@ describe Backup::Repository do
end
def list_repositories
- Dir[SEED_STORAGE_PATH + '/*.git']
+ Dir[File.join(SEED_STORAGE_PATH, '*.git')]
end
end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 10020511bf8..6eb10497428 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -64,4 +64,28 @@ describe Feature do
expect(described_class.all).to eq(features.to_a)
end
end
+
+ describe '.flipper' do
+ shared_examples 'a memoized Flipper instance' do
+ it 'memoizes the Flipper instance' do
+ expect(Flipper).to receive(:new).once.and_call_original
+
+ 2.times do
+ described_class.flipper
+ end
+ end
+ end
+
+ context 'when request store is inactive' do
+ before do
+ described_class.instance_variable_set(:@flipper, nil)
+ end
+
+ it_behaves_like 'a memoized Flipper instance'
+ end
+
+ context 'when request store is inactive', :request_store do
+ it_behaves_like 'a memoized Flipper instance'
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index 007e93c1db6..211e3aaa94b 100644
--- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
@@ -299,7 +299,11 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
let(:first_commit) { project.repository.commit(merge_request_diff.head_commit_sha) }
let(:expected_commits) { commits }
- let(:diffs) { first_commit.rugged_diff_from_parent.patches }
+ let(:diffs) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ first_commit.rugged_diff_from_parent.patches
+ end
+ end
let(:expected_diffs) { [] }
include_examples 'updated MR diff'
@@ -309,7 +313,11 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
let(:first_commit) { project.repository.commit(merge_request_diff.head_commit_sha) }
let(:expected_commits) { commits }
- let(:diffs) { first_commit.rugged_diff_from_parent.deltas }
+ let(:diffs) do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ first_commit.rugged_diff_from_parent.deltas
+ end
+ end
let(:expected_diffs) { [] }
include_examples 'updated MR diff'
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index 7201e4f7bf6..ec22e3a198e 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -6,7 +6,9 @@ describe Gitlab::Checks::LfsIntegrity do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:newrev) do
- operations = BareRepoOperations.new(repository.path)
+ operations = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ BareRepoOperations.new(repository.path)
+ end
# Create a commit not pointed at by any ref to emulate being in the
# pre-receive hook so that `--not --all` returns some objects
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
index 92792144429..5b343920429 100644
--- a/spec/lib/gitlab/conflict/file_spec.rb
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Conflict::File do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
- let(:rugged) { repository.rugged }
+ let(:rugged) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.rugged } }
let(:their_commit) { rugged.branches['conflict-start'].target }
let(:our_commit) { rugged.branches['conflict-resolvable'].target }
let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index dd5c498706d..7a9621d9c78 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -114,7 +114,9 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'raises a no repository exception when there is no repo' do
broken_repo = described_class.new('default', 'a/path.git', '')
- expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ expect do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access { broken_repo.rugged }
+ end.to raise_error(Gitlab::Git::Repository::NoRepository)
end
describe 'alternates keyword argument' do
@@ -124,9 +126,9 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it "is passed an empty array" do
- expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: [])
+ expect(Rugged::Repository).to receive(:new).with(repository_path, alternates: [])
- repository.rugged
+ repository_rugged
end
end
@@ -142,10 +144,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it "is passed the relative object dir envvars after being converted to absolute ones" do
- alternates = %w[foo bar baz].map { |d| File.join(repository.path, './objects', d) }
- expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: alternates)
+ alternates = %w[foo bar baz].map { |d| File.join(repository_path, './objects', d) }
+ expect(Rugged::Repository).to receive(:new).with(repository_path, alternates: alternates)
- repository.rugged
+ repository_rugged
end
end
end
@@ -156,16 +158,22 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:feature) { 'feature' }
let(:feature2) { 'feature2' }
+ around do |example|
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
it "returns 'master' when master exists" do
expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
expect(repository.discover_default_branch).to eq('master')
end
it "returns non-master when master exists but default branch is set to something else" do
- File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/feature')
+ File.write(File.join(repository_path, 'HEAD'), 'ref: refs/heads/feature')
expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
expect(repository.discover_default_branch).to eq('feature')
- File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/master')
+ File.write(File.join(repository_path, 'HEAD'), 'ref: refs/heads/master')
end
it "returns a non-master branch when only one exists" do
@@ -364,6 +372,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
context '#submodules' do
+ around do |example|
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
context 'where repo has submodules' do
@@ -474,8 +488,8 @@ describe Gitlab::Git::Repository, seed_helper: true do
# Sanity check
expect(repository.has_local_branches?).to eq(true)
- FileUtils.rm_rf(File.join(repository.path, 'packed-refs'))
- heads_dir = File.join(repository.path, 'refs/heads')
+ FileUtils.rm_rf(File.join(repository_path, 'packed-refs'))
+ heads_dir = File.join(repository_path, 'refs/heads')
FileUtils.rm_rf(heads_dir)
FileUtils.mkdir_p(heads_dir)
@@ -516,10 +530,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
branch_name = "to-be-deleted-soon"
repository.create_branch(branch_name)
- expect(repository.rugged.branches[branch_name]).not_to be_nil
+ expect(repository_rugged.branches[branch_name]).not_to be_nil
repository.delete_branch(branch_name)
- expect(repository.rugged.branches[branch_name]).to be_nil
+ expect(repository_rugged.branches[branch_name]).to be_nil
end
context "when branch does not exist" do
@@ -577,6 +591,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples 'deleting refs' do
let(:repo) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+ def repo_rugged
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repo.rugged
+ end
+ end
+
after do
ensure_seeds
end
@@ -584,7 +604,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'deletes the ref' do
repo.delete_refs('refs/heads/feature')
- expect(repo.rugged.references['refs/heads/feature']).to be_nil
+ expect(repo_rugged.references['refs/heads/feature']).to be_nil
end
it 'deletes all refs' do
@@ -592,7 +612,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
repo.delete_refs(*refs)
refs.each do |ref|
- expect(repo.rugged.references[ref]).to be_nil
+ expect(repo_rugged.references[ref]).to be_nil
end
end
@@ -615,7 +635,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#branch_names_contains_sha' do
- let(:head_id) { repository.rugged.head.target.oid }
+ let(:head_id) { repository_rugged.head.target.oid }
let(:new_branch) { head_id }
let(:utf8_branch) { 'branch-é' }
@@ -699,7 +719,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'fetches a repository as a mirror remote' do
subject
- expect(refs(new_repository.path)).to eq(refs(repository.path))
+ expect(refs(new_repository_path)).to eq(refs(repository_path))
end
context 'with keep-around refs' do
@@ -708,15 +728,15 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
before do
- repository.rugged.references.create(keep_around_ref, sha, force: true)
- repository.rugged.references.create(tmp_ref, sha, force: true)
+ repository_rugged.references.create(keep_around_ref, sha, force: true)
+ repository_rugged.references.create(tmp_ref, sha, force: true)
end
it 'includes the temporary and keep-around refs' do
subject
- expect(refs(new_repository.path)).to include(keep_around_ref)
- expect(refs(new_repository.path)).to include(tmp_ref)
+ expect(refs(new_repository_path)).to include(keep_around_ref)
+ expect(refs(new_repository_path)).to include(tmp_ref)
end
end
end
@@ -728,6 +748,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'with gitaly enabled', :skip_gitaly_mock do
it_behaves_like 'repository mirror fecthing'
end
+
+ def new_repository_path
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ new_repository.path
+ end
+ end
end
describe '#remote_tags' do
@@ -739,10 +765,17 @@ describe Gitlab::Git::Repository, seed_helper: true do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
+ around do |example|
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
subject { repository.remote_tags(remote_name) }
before do
- repository.add_remote(remote_name, remote_repository.path)
+ remote_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { remote_repository.path }
+ repository.add_remote(remote_name, remote_repository_path)
remote_repository.add_tag(tag_name, user: user, target: target_commit_id)
end
@@ -975,8 +1008,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
def commit_files(commit)
- commit.rugged_diff_from_parent.deltas.flat_map do |delta|
- [delta.old_file[:path], delta.new_file[:path]].uniq.compact
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ commit.rugged_diff_from_parent.deltas.flat_map do |delta|
+ [delta.old_file[:path], delta.new_file[:path]].uniq.compact
+ end
end
end
@@ -1019,6 +1054,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#rugged_commits_between" do
+ around do |example|
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
context 'two SHAs' do
let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' }
@@ -1363,7 +1404,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
allow(ref).to receive(:target) { raise Rugged::ReferenceError }
branches = double()
allow(branches).to receive(:each) { [ref].each }
- allow(repository.rugged).to receive(:branches) { branches }
+ allow(repository_rugged).to receive(:branches) { branches }
expect(subject).to be_empty
end
@@ -1661,6 +1702,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#batch_existence' do
let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] }
+ around do |example|
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ example.run
+ end
+ end
+
it 'returns existing refs back' do
result = repository.batch_existence(refs)
@@ -1840,7 +1887,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'when the branch exists' do
context 'when the commit does not exist locally' do
let(:source_branch) { 'new-branch-for-fetch-source-branch' }
- let(:source_rugged) { source_repository.rugged }
+ let(:source_rugged) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { source_repository.rugged } }
let(:new_oid) { new_commit_edit_old_file(source_rugged).oid }
before do
@@ -1898,7 +1945,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it "removes the branch from the repo" do
repository.rm_branch(branch_name, user: user)
- expect(repository.rugged.branches[branch_name]).to be_nil
+ expect(repository_rugged.branches[branch_name]).to be_nil
end
end
@@ -1930,7 +1977,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#write_config' do
before do
- repository.rugged.config["gitlab.fullpath"] = repository.path
+ repository_rugged.config["gitlab.fullpath"] = repository_path
end
shared_examples 'writing repo config' do
@@ -1938,7 +1985,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'writes it to disk' do
repository.write_config(full_path: "not-the/real-path.git")
- config = File.read(File.join(repository.path, "config"))
+ config = File.read(File.join(repository_path, "config"))
expect(config).to include("[gitlab]")
expect(config).to include("fullpath = not-the/real-path.git")
@@ -1949,10 +1996,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'does not write it to disk' do
repository.write_config(full_path: "")
- config = File.read(File.join(repository.path, "config"))
+ config = File.read(File.join(repository_path, "config"))
expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = #{repository.path}")
+ expect(config).to include("fullpath = #{repository_path}")
end
end
end
@@ -2173,7 +2220,11 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#gitlab_projects' do
subject { repository.gitlab_projects }
- it { expect(subject.shard_path).to eq(storage_path) }
+ it do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ expect(subject.shard_path).to eq(storage_path)
+ end
+ end
it { expect(subject.repository_relative_path).to eq(repository.relative_path) }
end
@@ -2189,7 +2240,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository.bundle_to_disk(save_path)
success = system(
- *%W(#{Gitlab.config.git.bin_path} -C #{repository.path} bundle verify #{save_path}),
+ *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}),
[:out, :err] => '/dev/null'
)
expect(success).to be true
@@ -2231,7 +2282,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
it 'creates a symlink to the global hooks dir' do
imported_repo.create_from_bundle(bundle_path)
- hooks_path = File.join(imported_repo.path, 'hooks')
+ hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
end
@@ -2360,7 +2411,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#clean_stale_repository_files' do
- let(:worktree_path) { File.join(repository.path, 'worktrees', 'delete-me') }
+ let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') }
it 'cleans up the files' do
repository.with_worktree(worktree_path, 'master', env: ENV) do
@@ -2507,7 +2558,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
- rugged = repository.rugged
+ rugged = repository_rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
end
@@ -2586,4 +2637,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
line.split("\t").last
end
end
+
+ def repository_rugged
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.rugged
+ end
+ end
+
+ def repository_path
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repository.path
+ end
+ end
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index a3b3dc3be6d..81dbbb962dd 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::UrlBlocker do
describe '#blocked_url?' do
- let(:valid_ports) { Project::VALID_IMPORT_PORTS }
+ let(:ports) { Project::VALID_IMPORT_PORTS }
it 'allows imports from configured web host and port' do
import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git"
@@ -19,7 +19,13 @@ describe Gitlab::UrlBlocker do
end
it 'returns true for bad port' do
- expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', valid_ports: valid_ports)).to be true
+ expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', ports: ports)).to be true
+ end
+
+ it 'returns true for bad protocol' do
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['https'])).to be false
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
index 23485fbcb18..88d6d0b559a 100644
--- a/spec/lib/omni_auth/strategies/jwt_spec.rb
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -43,7 +43,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
@@ -61,7 +61,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
@@ -80,7 +80,7 @@ describe OmniAuth::Strategies::Jwt do
end
it 'raises error' do
- expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::JWT::ClaimInvalid)
+ expect { strategy.decoded }.to raise_error(OmniAuth::Strategies::Jwt::ClaimInvalid)
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index f310a6854d5..69eafbe4bbe 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -422,7 +422,7 @@ describe Notify do
aggregate_failures do
is_expected.to have_referable_subject(merge_request, reply: true)
is_expected.to have_body_text(project_merge_request_path(project, merge_request))
- is_expected.to have_body_text('reasons:')
+ is_expected.to have_body_text('following reasons:')
reasons.each do |reason|
is_expected.to have_body_text(reason)
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index c89655f4058..66c9708b4cf 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -148,10 +148,9 @@ describe Ci::Build do
end
context 'when there are runners' do
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project, projects: [build.project]) }
before do
- build.project.runners << runner
runner.update_attributes(contacted_at: 1.second.ago)
end
@@ -1388,12 +1387,7 @@ describe Ci::Build do
it { is_expected.to be_truthy }
context "and there are specific runner" do
- let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
-
- before do
- build.project.runners << runner
- runner.save
- end
+ let!(:runner) { create(:ci_runner, :project, projects: [build.project], contacted_at: 1.second.ago) }
it { is_expected.to be_falsey }
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 2b9c232743d..24692ebb9a3 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1600,7 +1600,7 @@ describe Ci::Pipeline, :mailer do
context 'when pipeline is not stuck' do
before do
- create(:ci_runner, :shared, :online)
+ create(:ci_runner, :instance, :online)
end
it 'is not stuck' do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 0fbc934f669..0f072aa1719 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -21,60 +21,58 @@ describe Ci::Runner do
end
end
- context 'either_projects_or_group' do
+ context '#exactly_one_group' do
let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
- it 'disallows assigning to a group if already assigned to a group' do
- runner = create(:ci_runner, groups: [group])
-
+ it 'disallows assigning group if already assigned to a group' do
runner.groups << build(:group)
expect(runner).not_to be_valid
- expect(runner.errors.full_messages).to eq ['Runner can only be assigned to one group']
+ expect(runner.errors.full_messages).to include('Runner needs to be assigned to exactly one group')
end
+ end
- it 'disallows assigning to a group if already assigned to a project' do
- project = create(:project)
- runner = create(:ci_runner, projects: [project])
+ context 'runner_type validations' do
+ set(:group) { create(:group) }
+ set(:project) { create(:project) }
+ let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let(:instance_runner) { create(:ci_runner, :instance) }
- runner.groups << build(:group)
+ it 'disallows assigning group to project_type runner' do
+ project_runner.groups << build(:group)
- expect(runner).not_to be_valid
- expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
+ expect(project_runner).not_to be_valid
+ expect(project_runner.errors.full_messages).to include('Runner cannot have groups assigned')
end
- it 'disallows assigning to a project if already assigned to a group' do
- runner = create(:ci_runner, groups: [group])
-
- runner.projects << build(:project)
+ it 'disallows assigning group to instance_type runner' do
+ instance_runner.groups << build(:group)
- expect(runner).not_to be_valid
- expect(runner.errors.full_messages).to eq ['Runner can only be assigned either to projects or to a group']
+ expect(instance_runner).not_to be_valid
+ expect(instance_runner.errors.full_messages).to include('Runner cannot have groups assigned')
end
- it 'allows assigning to a group if not assigned to a group nor a project' do
- runner = create(:ci_runner)
-
- runner.groups << build(:group)
+ it 'disallows assigning project to group_type runner' do
+ group_runner.projects << build(:project)
- expect(runner).to be_valid
+ expect(group_runner).not_to be_valid
+ expect(group_runner.errors.full_messages).to include('Runner cannot have projects assigned')
end
- it 'allows assigning to a project if not assigned to a group nor a project' do
- runner = create(:ci_runner)
-
- runner.projects << build(:project)
+ it 'disallows assigning project to instance_type runner' do
+ instance_runner.projects << build(:project)
- expect(runner).to be_valid
+ expect(instance_runner).not_to be_valid
+ expect(instance_runner.errors.full_messages).to include('Runner cannot have projects assigned')
end
- it 'allows assigning to a project if already assigned to a project' do
- project = create(:project)
- runner = create(:ci_runner, projects: [project])
-
- runner.projects << build(:project)
+ it 'should fail to save a group assigned to a project runner even if the runner is already saved' do
+ group_runner
- expect(runner).to be_valid
+ expect { create(:group, runners: [project_runner]) }
+ .to raise_error(ActiveRecord::RecordInvalid)
end
end
end
@@ -110,17 +108,12 @@ describe Ci::Runner do
describe '.shared' do
let(:group) { create(:group) }
let(:project) { create(:project) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let!(:shared_runner) { create(:ci_runner, :instance) }
- it 'returns the shared group runner' do
- runner = create(:ci_runner, :shared, groups: [group])
-
- expect(described_class.shared).to eq [runner]
- end
-
- it 'returns the shared project runner' do
- runner = create(:ci_runner, :shared, projects: [project])
-
- expect(described_class.shared).to eq [runner]
+ it 'returns only shared runners' do
+ expect(described_class.shared).to contain_exactly(shared_runner)
end
end
@@ -128,11 +121,11 @@ describe Ci::Runner do
it 'returns the specific project runner' do
# own
specific_project = create(:project)
- specific_runner = create(:ci_runner, :specific, projects: [specific_project])
+ specific_runner = create(:ci_runner, :project, projects: [specific_project])
# other
other_project = create(:project)
- create(:ci_runner, :specific, projects: [other_project])
+ create(:ci_runner, :project, projects: [other_project])
expect(described_class.belonging_to_project(specific_project.id)).to eq [specific_runner]
end
@@ -141,17 +134,17 @@ describe Ci::Runner do
describe '.belonging_to_parent_group_of_project' do
let(:project) { create(:project, group: group) }
let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, :specific, groups: [group]) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
let!(:unrelated_group) { create(:group) }
let!(:unrelated_project) { create(:project, group: unrelated_group) }
- let!(:unrelated_runner) { create(:ci_runner, :specific, groups: [unrelated_group]) }
+ let!(:unrelated_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
it 'returns the specific group runner' do
expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner)
end
context 'with a parent group with a runner', :nested_groups do
- let(:runner) { create(:ci_runner, :specific, groups: [parent_group]) }
+ let(:runner) { create(:ci_runner, :group, groups: [parent_group]) }
let(:project) { create(:project, group: group) }
let(:group) { create(:group, parent: parent_group) }
let(:parent_group) { create(:group) }
@@ -167,13 +160,13 @@ describe Ci::Runner do
# group specific
group = create(:group)
project = create(:project, group: group)
- group_runner = create(:ci_runner, :specific, groups: [group])
+ group_runner = create(:ci_runner, :group, groups: [group])
# project specific
- project_runner = create(:ci_runner, :specific, projects: [project])
+ project_runner = create(:ci_runner, :project, projects: [project])
# globally shared
- shared_runner = create(:ci_runner, :shared)
+ shared_runner = create(:ci_runner, :instance)
expect(described_class.owned_or_shared(project.id)).to contain_exactly(
group_runner, project_runner, shared_runner
@@ -183,31 +176,32 @@ describe Ci::Runner do
describe '#display_name' do
it 'returns the description if it has a value' do
- runner = FactoryBot.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
+ runner = build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448'
end
it 'returns the token if it does not have a description' do
- runner = FactoryBot.create(:ci_runner)
+ runner = create(:ci_runner)
expect(runner.display_name).to eq runner.description
end
it 'returns the token if the description is an empty string' do
- runner = FactoryBot.build(:ci_runner, description: '', token: 'token')
+ runner = build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token
end
end
describe '#assign_to' do
- let!(:project) { FactoryBot.create(:project) }
+ let(:project) { create(:project) }
subject { runner.assign_to(project) }
context 'with shared_runner' do
- let!(:runner) { FactoryBot.create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
it 'transitions shared runner to project runner and assigns project' do
- subject
+ expect(subject).to be_truthy
+
expect(runner).to be_specific
expect(runner).to be_project_type
expect(runner.projects).to eq([project])
@@ -216,7 +210,8 @@ describe Ci::Runner do
end
context 'with group runner' do
- let!(:runner) { FactoryBot.create(:ci_runner, runner_type: :group_type) }
+ let(:group) { create(:group) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
it 'raises an error' do
expect { subject }
@@ -229,15 +224,15 @@ describe Ci::Runner do
subject { described_class.online }
before do
- @runner1 = FactoryBot.create(:ci_runner, :shared, contacted_at: 1.year.ago)
- @runner2 = FactoryBot.create(:ci_runner, :shared, contacted_at: 1.second.ago)
+ @runner1 = create(:ci_runner, :instance, contacted_at: 1.year.ago)
+ @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago)
end
it { is_expected.to eq([@runner2])}
end
describe '#online?' do
- let(:runner) { FactoryBot.create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
subject { runner.online? }
@@ -307,21 +302,20 @@ describe Ci::Runner do
end
describe '#can_pick?' do
- let(:pipeline) { create(:ci_pipeline) }
+ set(:pipeline) { create(:ci_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:runner) { create(:ci_runner, tag_list: tag_list, run_untagged: run_untagged) }
+ let(:runner_project) { build.project }
+ let(:runner) { create(:ci_runner, :project, projects: [runner_project], tag_list: tag_list, run_untagged: run_untagged) }
let(:tag_list) { [] }
let(:run_untagged) { true }
subject { runner.can_pick?(build) }
- before do
- build.project.runners << runner
- end
-
context 'a different runner' do
+ let(:other_project) { create(:project) }
+ let(:other_runner) { create(:ci_runner, :project, projects: [other_project], tag_list: tag_list, run_untagged: run_untagged) }
+
it 'cannot handle builds' do
- other_runner = create(:ci_runner)
expect(other_runner.can_pick?(build)).to be_falsey
end
end
@@ -375,18 +369,14 @@ describe Ci::Runner do
end
context 'when runner is shared' do
- let(:runner) { create(:ci_runner, :shared) }
-
- before do
- build.project.runners = []
- end
+ let(:runner) { create(:ci_runner, :instance) }
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
end
context 'when runner is locked' do
- let(:runner) { create(:ci_runner, :shared, locked: true) }
+ let(:runner) { create(:ci_runner, :instance, locked: true) }
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
@@ -401,10 +391,8 @@ describe Ci::Runner do
end
end
- context 'when runner is not assigned to a project' do
- before do
- build.project.runners = []
- end
+ context 'when runner is assigned to another project' do
+ let(:runner_project) { create(:project) }
it 'cannot handle builds' do
expect(runner.can_pick?(build)).to be_falsey
@@ -412,10 +400,8 @@ describe Ci::Runner do
end
context 'when runner is assigned to a group' do
- before do
- build.project.runners = []
- runner.groups << create(:group, projects: [build.project])
- end
+ let(:group) { create(:group, projects: [build.project]) }
+ let(:runner) { create(:ci_runner, :group, tag_list: tag_list, run_untagged: run_untagged, groups: [group]) }
it 'can handle builds' do
expect(runner.can_pick?(build)).to be_truthy
@@ -469,7 +455,7 @@ describe Ci::Runner do
end
describe '#status' do
- let(:runner) { FactoryBot.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
+ let(:runner) { create(:ci_runner, :instance, contacted_at: 1.second.ago) }
subject { runner.status }
@@ -626,12 +612,13 @@ describe Ci::Runner do
end
describe '.assignable_for' do
- let!(:unlocked_project_runner) { create(:ci_runner, runner_type: :project_type, projects: [project]) }
- let!(:locked_project_runner) { create(:ci_runner, runner_type: :project_type, locked: true, projects: [project]) }
- let!(:group_runner) { create(:ci_runner, runner_type: :group_type) }
- let!(:instance_runner) { create(:ci_runner, :shared) }
let(:project) { create(:project) }
+ let(:group) { create(:group) }
let(:another_project) { create(:project) }
+ let!(:unlocked_project_runner) { create(:ci_runner, :project, projects: [project]) }
+ let!(:locked_project_runner) { create(:ci_runner, :project, locked: true, projects: [project]) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:instance_runner) { create(:ci_runner, :instance) }
context 'with already assigned project' do
subject { described_class.assignable_for(project) }
@@ -651,19 +638,16 @@ describe Ci::Runner do
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
- runner = FactoryBot.create(:ci_runner)
- project = FactoryBot.create(:project)
- project1 = FactoryBot.create(:project)
- project.runners << runner
- project1.runners << runner
+ project1 = create(:project)
+ project2 = create(:project)
+ runner = create(:ci_runner, :project, projects: [project1, project2])
expect(runner.belongs_to_one_project?).to be_falsey
end
it "returns true" do
- runner = FactoryBot.create(:ci_runner)
- project = FactoryBot.create(:project)
- project.runners << runner
+ project = create(:project)
+ runner = create(:ci_runner, :project, projects: [project])
expect(runner.belongs_to_one_project?).to be_truthy
end
@@ -713,21 +697,21 @@ describe Ci::Runner do
subject { runner.assigned_to_group? }
context 'when project runner' do
- let(:runner) { create(:ci_runner, description: 'Project runner', projects: [project]) }
+ let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
let(:project) { create(:project) }
it { is_expected.to be_falsey }
end
context 'when shared runner' do
- let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
+ let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') }
it { is_expected.to be_falsey }
end
context 'when group runner' do
let(:group) { create(:group) }
- let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
+ let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
it { is_expected.to be_truthy }
end
@@ -737,18 +721,18 @@ describe Ci::Runner do
subject { runner.assigned_to_project? }
context 'when group runner' do
- let(:runner) { create(:ci_runner, description: 'Group runner', groups: [group]) }
+ let(:runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
let(:group) { create(:group) }
it { is_expected.to be_falsey }
end
context 'when shared runner' do
- let(:runner) { create(:ci_runner, :shared, description: 'Shared runner') }
+ let(:runner) { create(:ci_runner, :instance, description: 'Shared runner') }
it { is_expected.to be_falsey }
end
context 'when project runner' do
- let(:runner) { create(:ci_runner, description: 'Group runner', projects: [project]) }
+ let(:runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
let(:project) { create(:project) }
it { is_expected.to be_truthy }
@@ -780,4 +764,17 @@ describe Ci::Runner do
end
end
end
+
+ describe 'project runner without projects is destroyable' do
+ subject { create(:ci_runner, :project, :without_projects) }
+
+ it 'does not have projects' do
+ expect(subject.runner_projects).to be_empty
+ end
+
+ it 'can be destroyed' do
+ subject
+ expect { subject.destroy }.to change { described_class.count }.by(-1)
+ end
+ end
end
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
new file mode 100644
index 00000000000..ca48a1d8072
--- /dev/null
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -0,0 +1,59 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Jupyter do
+ include_examples 'cluster application core specs', :clusters_applications_jupyter
+
+ it { is_expected.to belong_to(:oauth_application) }
+
+ describe '#set_initial_status' do
+ before do
+ jupyter.set_initial_status
+ end
+
+ context 'when ingress is not installed' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+
+ it { expect(jupyter).to be_not_installable }
+ end
+
+ context 'when ingress is installed and external_ip is assigned' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ it { expect(jupyter).to be_installable }
+ end
+ end
+
+ describe '#install_command' do
+ let!(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ subject { jupyter.install_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
+
+ it 'should be initialized with 4 arguments' do
+ expect(subject.name).to eq('jupyter')
+ expect(subject.chart).to eq('jupyter/jupyterhub')
+ expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/')
+ expect(subject.values).to eq(jupyter.values)
+ end
+ end
+
+ describe '#values' do
+ let(:jupyter) { create(:clusters_applications_jupyter) }
+
+ subject { jupyter.values }
+
+ it 'should include valid values' do
+ is_expected.to include('ingress')
+ is_expected.to include('hub')
+ is_expected.to include('rbac')
+ is_expected.to include('proxy')
+ is_expected.to include('auth')
+ is_expected.to include("clientId: #{jupyter.oauth_application.uid}")
+ is_expected.to include("callbackUrl: #{jupyter.callback_url}")
+ end
+ end
+end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index b942554d67b..6f66515b45f 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -234,9 +234,10 @@ describe Clusters::Cluster do
let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus, runner)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter)
end
end
end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index 49e4b23ebc7..c6331c5ec15 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -22,7 +22,7 @@ describe CacheableAttributes do
attr_accessor :attributes
- def initialize(attrs = {})
+ def initialize(attrs = {}, *)
@attributes = attrs
end
end
@@ -52,7 +52,7 @@ describe CacheableAttributes do
describe '.cache_key' do
it 'excludes cache attributes' do
- expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:json")
+ expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:#{Rails.version}")
end
end
@@ -75,49 +75,117 @@ describe CacheableAttributes do
context 'without any attributes given' do
it 'intializes a new object with the defaults' do
- expect(minimal_test_class.build_from_defaults).not_to be_persisted
+ expect(minimal_test_class.build_from_defaults.attributes).to eq(minimal_test_class.defaults)
end
end
- context 'without attributes given' do
+ context 'with attributes given' do
it 'intializes a new object with the given attributes merged into the defaults' do
expect(minimal_test_class.build_from_defaults(foo: 'd').attributes[:foo]).to eq('d')
end
end
+
+ describe 'edge cases on concrete implementations' do
+ describe '.build_from_defaults' do
+ context 'without any attributes given' do
+ it 'intializes all attributes even if they are nil' do
+ record = ApplicationSetting.build_from_defaults
+
+ expect(record).not_to be_persisted
+ expect(record.sign_in_text).to be_nil
+ end
+ end
+ end
+ end
end
describe '.current', :use_clean_rails_memory_store_caching do
context 'redis unavailable' do
- it 'returns an uncached record' do
+ before do
allow(minimal_test_class).to receive(:last).and_return(:last)
- expect(Rails.cache).to receive(:read).and_raise(Redis::BaseError)
+ expect(Rails.cache).to receive(:read).with(minimal_test_class.cache_key).and_raise(Redis::BaseError)
+ end
+
+ context 'in production environment' do
+ before do
+ expect(Rails.env).to receive(:production?).and_return(true)
+ end
+
+ it 'returns an uncached record and logs a warning' do
+ expect(Rails.logger).to receive(:warn).with("Cached record for TestClass couldn't be loaded, falling back to uncached record: Redis::BaseError")
- expect(minimal_test_class.current).to eq(:last)
+ expect(minimal_test_class.current).to eq(:last)
+ end
+ end
+
+ context 'in other environments' do
+ before do
+ expect(Rails.env).to receive(:production?).and_return(false)
+ end
+
+ it 'returns an uncached record and logs a warning' do
+ expect(Rails.logger).not_to receive(:warn)
+
+ expect { minimal_test_class.current }.to raise_error(Redis::BaseError)
+ end
end
end
context 'when a record is not yet present' do
it 'does not cache nil object' do
# when missing settings a nil object is returned, but not cached
- allow(minimal_test_class).to receive(:last).twice.and_return(nil)
+ allow(ApplicationSetting).to receive(:current_without_cache).twice.and_return(nil)
- expect(minimal_test_class.current).to be_nil
- expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(false)
+ expect(ApplicationSetting.current).to be_nil
+ expect(Rails.cache.exist?(ApplicationSetting.cache_key)).to be(false)
end
- it 'cache non-nil object' do
- # when the settings are set the method returns a valid object
- allow(minimal_test_class).to receive(:last).and_call_original
+ it 'caches non-nil object' do
+ create(:application_setting)
- expect(minimal_test_class.current).to eq(minimal_test_class.last)
- expect(Rails.cache.exist?(minimal_test_class.cache_key)).to be(true)
+ expect(ApplicationSetting.current).to eq(ApplicationSetting.last)
+ expect(Rails.cache.exist?(ApplicationSetting.cache_key)).to be(true)
# subsequent calls retrieve the record from the cache
- last_record = minimal_test_class.last
- expect(minimal_test_class).not_to receive(:last)
- expect(minimal_test_class.current.attributes).to eq(last_record.attributes)
+ last_record = ApplicationSetting.last
+ expect(ApplicationSetting).not_to receive(:current_without_cache)
+ expect(ApplicationSetting.current.attributes).to eq(last_record.attributes)
end
end
+
+ describe 'edge cases' do
+ describe 'caching behavior', :use_clean_rails_memory_store_caching do
+ it 'retrieves upload fields properly' do
+ ar_record = create(:appearance, :with_logo)
+ ar_record.cache!
+
+ cache_record = Appearance.current
+
+ expect(cache_record).to be_persisted
+ expect(cache_record.logo).to be_an(AttachmentUploader)
+ expect(cache_record.logo.url).to end_with('/dk.png')
+ end
+
+ it 'retrieves markdown fields properly' do
+ ar_record = create(:appearance, description: '**Hello**')
+ ar_record.cache!
+
+ cache_record = Appearance.current
+
+ expect(cache_record.description).to eq('**Hello**')
+ expect(cache_record.description_html).to eq('<p dir="auto"><strong>Hello</strong></p>')
+ end
+ end
+ end
+
+ it 'uses RequestStore in addition to Rails.cache', :request_store do
+ # Warm up the cache
+ create(:application_setting).cache!
+
+ expect(Rails.cache).to receive(:read).with(ApplicationSetting.cache_key).once.and_call_original
+
+ 2.times { ApplicationSetting.current }
+ end
end
describe '.cached', :use_clean_rails_memory_store_caching do
@@ -127,27 +195,36 @@ describe CacheableAttributes do
end
end
- context 'when cached settings do not include the latest defaults' do
+ context 'when cached is warm' do
before do
- Rails.cache.write(minimal_test_class.cache_key, { bar: 'b', baz: 'c' }.to_json)
- minimal_test_class.define_singleton_method(:defaults) do
- { foo: 'a', bar: 'b', baz: 'c' }
- end
+ # Warm up the cache
+ create(:appearance).cache!
end
- it 'includes attributes from defaults' do
- expect(minimal_test_class.cached.attributes[:foo]).to eq(minimal_test_class.defaults[:foo])
+ it 'retrieves the record from cache' do
+ expect(ActiveRecord::QueryRecorder.new { Appearance.cached }.count).to eq(0)
+ expect(Appearance.cached).to eq(Appearance.current_without_cache)
end
end
end
describe '#cache!', :use_clean_rails_memory_store_caching do
- let(:appearance_record) { create(:appearance) }
+ let(:record) { create(:appearance) }
it 'caches the attributes' do
- appearance_record.cache!
+ record.cache!
- expect(Rails.cache.read(Appearance.cache_key)).to eq(appearance_record.attributes.to_json)
+ expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
+ end
+
+ describe 'edge cases' do
+ let(:record) { create(:appearance) }
+
+ it 'caches the attributes' do
+ record.cache!
+
+ expect(Rails.cache.read(Appearance.cache_key)).to eq(record)
+ end
end
end
end
diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/has_variable_spec.rb
index f87869a2fdc..3fbe86c5b56 100644
--- a/spec/models/concerns/has_variable_spec.rb
+++ b/spec/models/concerns/has_variable_spec.rb
@@ -45,8 +45,10 @@ describe HasVariable do
end
it 'fails to decrypt if iv is incorrect' do
- subject.encrypted_value_iv = SecureRandom.hex
+ # attr_encrypted expects the IV to be 16 bytes and base64-encoded
+ subject.encrypted_value_iv = [SecureRandom.hex(8)].pack('m')
subject.instance_variable_set(:@value, nil)
+
expect { subject.value }
.to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index bd6bf5b0712..1cfd526834c 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -12,6 +12,7 @@ describe Issuable do
it { is_expected.to belong_to(:author) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:labels) }
context 'Notes' do
let!(:note) { create(:note, noteable: issue, project: issue.project) }
@@ -274,8 +275,8 @@ describe Issuable do
it 'skips coercion for not Integer values' do
expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
- expect { issue.time_estimate = 'invalid time' }.not_to raise_error(StandardError)
- expect { issue.time_estimate = 22.33 }.not_to raise_error(StandardError)
+ expect { issue.time_estimate = 'invalid time' }.not_to raise_error
+ expect { issue.time_estimate = 22.33 }.not_to raise_error
end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 4570dbb1d8e..f2a3df50c1a 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -94,6 +94,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
it { expect(instance.result).to be_nil }
+ it { expect(reactive_cache_alive?(instance)).to be_falsy }
end
describe '#exclusively_update_reactive_cache!' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index af2240f4f89..9a76452a808 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1177,8 +1177,8 @@ describe Project do
describe '#any_runners?' do
context 'shared runners' do
let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
- let(:specific_runner) { create(:ci_runner) }
- let(:shared_runner) { create(:ci_runner, :shared) }
+ let(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ let(:shared_runner) { create(:ci_runner, :instance) }
context 'for shared runners disabled' do
let(:shared_runners_enabled) { false }
@@ -1188,7 +1188,7 @@ describe Project do
end
it 'has a specific runner' do
- project.runners << specific_runner
+ specific_runner
expect(project.any_runners?).to be_truthy
end
@@ -1200,13 +1200,13 @@ describe Project do
end
it 'checks the presence of specific runner' do
- project.runners << specific_runner
+ specific_runner
expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
end
it 'returns false if match cannot be found' do
- project.runners << specific_runner
+ specific_runner
expect(project.any_runners? { false }).to be_falsey
end
@@ -1238,7 +1238,7 @@ describe Project do
context 'group runners' do
let(:project) { create(:project, group_runners_enabled: group_runners_enabled) }
let(:group) { create(:group, projects: [project]) }
- let(:group_runner) { create(:ci_runner, groups: [group]) }
+ let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
context 'for group runners disabled' do
let(:group_runners_enabled) { false }
@@ -1279,7 +1279,7 @@ describe Project do
end
describe '#shared_runners' do
- let!(:runner) { create(:ci_runner, :shared) }
+ let!(:runner) { create(:ci_runner, :instance) }
subject { project.shared_runners }
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index a80800c6c92..1d94abe4195 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -12,8 +12,8 @@ describe RemoteMirror do
context 'with an invalid URL' do
it 'should not be valid' do
remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid')
+
expect(remote_mirror).not_to be_valid
- expect(remote_mirror.errors[:url].size).to eq(2)
end
end
end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index 6e30798356c..a0c93c531ea 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -5,6 +5,9 @@ RSpec.describe Timelog do
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
+ it { is_expected.to belong_to(:issue).touch(true) }
+ it { is_expected.to belong_to(:merge_request).touch(true) }
+
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:time_spent) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 16b409844fa..09dfeae6377 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1858,13 +1858,10 @@ describe User do
describe '#ci_owned_runners' do
let(:user) { create(:user) }
- let(:runner_1) { create(:ci_runner) }
- let(:runner_2) { create(:ci_runner) }
+ let!(:project) { create(:project) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
context 'without any projects nor groups' do
- let!(:project) { create(:project, runners: [runner_1]) }
- let!(:group) { create(:group) }
-
it 'does not load' do
expect(user.ci_owned_runners).to be_empty
end
@@ -1872,38 +1869,40 @@ describe User do
context 'with personal projects runners' do
let(:namespace) { create(:namespace, owner: user) }
- let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
+ let!(:project) { create(:project, namespace: namespace) }
it 'loads' do
- expect(user.ci_owned_runners).to contain_exactly(runner_1)
+ expect(user.ci_owned_runners).to contain_exactly(runner)
end
end
context 'with personal group runner' do
- let!(:project) { create(:project, runners: [runner_1]) }
+ let!(:project) { create(:project) }
+ let(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:group) do
- create(:group, runners: [runner_2]).tap do |group|
+ create(:group).tap do |group|
group.add_owner(user)
end
end
it 'loads' do
- expect(user.ci_owned_runners).to contain_exactly(runner_2)
+ expect(user.ci_owned_runners).to contain_exactly(group_runner)
end
end
context 'with personal project and group runner' do
let(:namespace) { create(:namespace, owner: user) }
- let!(:project) { create(:project, namespace: namespace, runners: [runner_1]) }
+ let!(:project) { create(:project, namespace: namespace) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:group) do
- create(:group, runners: [runner_2]).tap do |group|
+ create(:group).tap do |group|
group.add_owner(user)
end
end
it 'loads' do
- expect(user.ci_owned_runners).to contain_exactly(runner_1, runner_2)
+ expect(user.ci_owned_runners).to contain_exactly(runner, group_runner)
end
end
@@ -1914,7 +1913,7 @@ describe User do
end
it 'loads' do
- expect(user.ci_owned_runners).to contain_exactly(runner_1)
+ expect(user.ci_owned_runners).to contain_exactly(runner)
end
end
@@ -1931,7 +1930,7 @@ describe User do
context 'with groups projects runners' do
let(:group) { create(:group) }
- let!(:project) { create(:project, group: group, runners: [runner_1]) }
+ let!(:project) { create(:project, group: group) }
def add_user(access)
group.add_user(user, access)
@@ -1941,11 +1940,8 @@ describe User do
end
context 'with groups runners' do
- let!(:group) do
- create(:group, runners: [runner_1]).tap do |group|
- group.add_owner(user)
- end
- end
+ let!(:runner) { create(:ci_runner, :group, groups: [group]) }
+ let!(:group) { create(:group) }
def add_user(access)
group.add_user(user, access)
@@ -1955,7 +1951,7 @@ describe User do
end
context 'with other projects runners' do
- let!(:project) { create(:project, runners: [runner_1]) }
+ let!(:project) { create(:project) }
def add_user(access)
project.add_role(user, access)
@@ -1968,7 +1964,7 @@ describe User do
let(:group) { create(:group) }
let(:another_user) { create(:user) }
let(:subgroup) { create(:group, parent: group) }
- let!(:project) { create(:project, group: subgroup, runners: [runner_1]) }
+ let!(:project) { create(:project, group: subgroup) }
def add_user(access)
group.add_user(user, access)
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index f246bb79ab7..cd43bec35df 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -304,7 +304,7 @@ describe API::CommitStatuses do
it 'responds with bad request status and validation errors' do
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['target_url'])
- .to include 'must be a valid URL'
+ .to include 'is blocked: Only allowed protocols are http, https'
end
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 3106083293f..4181f4ebbbe 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -1351,19 +1351,25 @@ describe API::Issues do
expect(json_response['labels']).to eq([label.title])
end
- it 'removes all labels' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
+ it 'removes all labels and touches the record' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: ''
+ end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to eq([])
+ expect(json_response['updated_at']).to be > Time.now
end
- it 'updates labels' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ it 'updates labels and touches the record' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
labels: 'foo,bar'
+ end
expect(response).to have_gitlab_http_status(200)
expect(json_response['labels']).to include 'foo'
expect(json_response['labels']).to include 'bar'
+ expect(json_response['updated_at']).to be > Time.now
end
it 'allows special label names' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 6aadf839dbd..319ac389083 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -111,11 +111,13 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when tags are not provided' do
- it 'returns 404 error' do
+ it 'returns 400 error' do
post api('/runners'), token: registration_token,
run_untagged: false
- expect(response).to have_gitlab_http_status 404
+ expect(response).to have_gitlab_http_status 400
+ expect(json_response['message']).to include(
+ 'tags_list' => ['can not be empty when runner is not allowed to pick untagged jobs'])
end
end
end
@@ -262,16 +264,12 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
describe '/api/v4/jobs' do
let(:project) { create(:project, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:job) do
create(:ci_build, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
end
- before do
- project.runners << runner
- end
-
describe 'POST /api/v4/jobs/request' do
let!(:last_update) {}
let!(:new_update) { }
@@ -379,7 +377,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when shared runner requests job for project without shared_runners_enabled' do
- let(:runner) { create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
it_behaves_like 'no jobs available'
end
@@ -724,7 +722,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when runner specifies lower timeout' do
- let(:runner) { create(:ci_runner, maximum_timeout: 1000) }
+ let(:runner) { create(:ci_runner, :project, maximum_timeout: 1000, projects: [project]) }
it 'contains info about timeout overridden by runner' do
request_job
@@ -735,7 +733,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when runner specifies bigger timeout' do
- let(:runner) { create(:ci_runner, maximum_timeout: 2000) }
+ let(:runner) { create(:ci_runner, :project, maximum_timeout: 2000, projects: [project]) }
it 'contains info about timeout not overridden by runner' do
request_job
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index c7587c877fc..0c7937feed6 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -11,23 +11,10 @@ describe API::Runners do
let(:group) { create(:group).tap { |group| group.add_owner(user) } }
let(:group2) { create(:group).tap { |group| group.add_owner(user) } }
- let!(:shared_runner) { create(:ci_runner, :shared, description: 'Shared runner') }
- let!(:unused_project_runner) { create(:ci_runner) }
-
- let!(:project_runner) do
- create(:ci_runner, description: 'Project runner').tap do |runner|
- create(:ci_runner_project, runner: runner, project: project)
- end
- end
-
- let!(:two_projects_runner) do
- create(:ci_runner, description: 'Two projects runner').tap do |runner|
- create(:ci_runner_project, runner: runner, project: project)
- create(:ci_runner_project, runner: runner, project: project2)
- end
- end
-
- let!(:group_runner) { create(:ci_runner, description: 'Group runner', groups: [group], runner_type: :group_type) }
+ let!(:shared_runner) { create(:ci_runner, :instance, description: 'Shared runner') }
+ let!(:project_runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
+ let!(:two_projects_runner) { create(:ci_runner, :project, description: 'Two projects runner', projects: [project, project2]) }
+ let!(:group_runner) { create(:ci_runner, :group, description: 'Group runner', groups: [group]) }
before do
# Set project access for users
@@ -141,6 +128,18 @@ describe API::Runners do
end
context 'when runner is not shared' do
+ context 'when unused runner is present' do
+ let!(:unused_project_runner) { create(:ci_runner, :project, :without_projects) }
+
+ it 'deletes unused runner' do
+ expect do
+ delete api("/runners/#{unused_project_runner.id}", admin)
+
+ expect(response).to have_gitlab_http_status(204)
+ end.to change { Ci::Runner.specific.count }.by(-1)
+ end
+ end
+
it "returns runner's details" do
get api("/runners/#{project_runner.id}", admin)
@@ -310,14 +309,6 @@ describe API::Runners do
end
context 'when runner is not shared' do
- it 'deletes unused runner' do
- expect do
- delete api("/runners/#{unused_project_runner.id}", admin)
-
- expect(response).to have_gitlab_http_status(204)
- end.to change { Ci::Runner.specific.count }.by(-1)
- end
-
it 'deletes used project runner' do
expect do
delete api("/runners/#{project_runner.id}", admin)
@@ -543,11 +534,7 @@ describe API::Runners do
describe 'POST /projects/:id/runners' do
context 'authorized user' do
- let(:project_runner2) do
- create(:ci_runner).tap do |runner|
- create(:ci_runner_project, runner: runner, project: project2)
- end
- end
+ let(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
it 'enables specific runner' do
expect do
@@ -560,7 +547,7 @@ describe API::Runners do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: project_runner.id
end.to change { project.runners.count }.by(0)
- expect(response).to have_gitlab_http_status(409)
+ expect(response).to have_gitlab_http_status(400)
end
it 'does not enable locked runner' do
@@ -586,11 +573,15 @@ describe API::Runners do
end
context 'user is admin' do
- it 'enables any specific runner' do
- expect do
- post api("/projects/#{project.id}/runners", admin), runner_id: unused_project_runner.id
- end.to change { project.runners.count }.by(+1)
- expect(response).to have_gitlab_http_status(201)
+ context 'when project runner is used' do
+ let!(:new_project_runner) { create(:ci_runner, :project) }
+
+ it 'enables any specific runner' do
+ expect do
+ post api("/projects/#{project.id}/runners", admin), runner_id: new_project_runner.id
+ end.to change { project.runners.count }.by(+1)
+ expect(response).to have_gitlab_http_status(201)
+ end
end
it 'enables a shared runner' do
@@ -603,14 +594,6 @@ describe API::Runners do
end
end
- context 'user is not admin' do
- it 'does not enable runner without access to' do
- post api("/projects/#{project.id}/runners", user), runner_id: unused_project_runner.id
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
it 'raises an error when no runner_id param is provided' do
post api("/projects/#{project.id}/runners", admin)
@@ -618,6 +601,16 @@ describe API::Runners do
end
end
+ context 'user is not admin' do
+ let!(:new_project_runner) { create(:ci_runner, :project) }
+
+ it 'does not enable runner without access to' do
+ post api("/projects/#{project.id}/runners", user), runner_id: new_project_runner.id
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
context 'authorized user without permissions' do
it 'does not enable runner' do
post api("/projects/#{project.id}/runners", user2)
diff --git a/spec/serializers/runner_entity_spec.rb b/spec/serializers/runner_entity_spec.rb
index 439ba2cbca2..ba99d568eba 100644
--- a/spec/serializers/runner_entity_spec.rb
+++ b/spec/serializers/runner_entity_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
describe RunnerEntity do
- let(:runner) { create(:ci_runner, :specific) }
+ let(:project) { create(:project) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:entity) { described_class.new(runner, request: request, current_user: user) }
let(:request) { double('request') }
- let(:project) { create(:project) }
let(:user) { create(:admin) }
before do
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 8063bc7e1ac..3816bd0deb5 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -5,15 +5,11 @@ module Ci
set(:group) { create(:group) }
set(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
set(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:shared_runner) { create(:ci_runner, is_shared: true) }
- let!(:specific_runner) { create(:ci_runner, is_shared: false) }
- let!(:group_runner) { create(:ci_runner, groups: [group], runner_type: :group_type) }
+ let!(:shared_runner) { create(:ci_runner, :instance) }
+ let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
+ let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
- before do
- specific_runner.assign_to(project)
- end
-
describe '#execute' do
context 'runner follow tag list' do
it "picks build with the same tag" do
@@ -181,24 +177,24 @@ module Ci
end
context 'for multiple builds' do
- let!(:project2) { create :project, group_runners_enabled: true, group: group }
- let!(:pipeline2) { create :ci_pipeline, project: project2 }
- let!(:project3) { create :project, group_runners_enabled: true, group: group }
- let!(:pipeline3) { create :ci_pipeline, project: project3 }
+ let!(:project2) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline2) { create(:ci_pipeline, project: project2) }
+ let!(:project3) { create(:project, group_runners_enabled: true, group: group) }
+ let!(:pipeline3) { create(:ci_pipeline, project: project3) }
let!(:build1_project1) { pending_job }
- let!(:build2_project1) { create :ci_build, pipeline: pipeline }
- let!(:build3_project1) { create :ci_build, pipeline: pipeline }
- let!(:build1_project2) { create :ci_build, pipeline: pipeline2 }
- let!(:build2_project2) { create :ci_build, pipeline: pipeline2 }
- let!(:build1_project3) { create :ci_build, pipeline: pipeline3 }
+ let!(:build2_project1) { create(:ci_build, pipeline: pipeline) }
+ let!(:build3_project1) { create(:ci_build, pipeline: pipeline) }
+ let!(:build1_project2) { create(:ci_build, pipeline: pipeline2) }
+ let!(:build2_project2) { create(:ci_build, pipeline: pipeline2) }
+ let!(:build1_project3) { create(:ci_build, pipeline: pipeline3) }
# these shouldn't influence the scheduling
- let!(:unrelated_group) { create :group }
- let!(:unrelated_project) { create :project, group_runners_enabled: true, group: unrelated_group }
- let!(:unrelated_pipeline) { create :ci_pipeline, project: unrelated_project }
- let!(:build1_unrelated_project) { create :ci_build, pipeline: unrelated_pipeline }
- let!(:unrelated_group_runner) { create :ci_runner, groups: [unrelated_group] }
+ let!(:unrelated_group) { create(:group) }
+ let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) }
+ let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) }
+ let!(:build1_unrelated_project) { create(:ci_build, pipeline: unrelated_pipeline) }
+ let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) }
it 'does not consider builds from other group runners' do
expect(described_class.new(group_runner).send(:builds_for_group_runner).count).to eq 6
@@ -292,7 +288,7 @@ module Ci
end
context 'when access_level of runner is not_protected' do
- let!(:specific_runner) { create(:ci_runner, :specific) }
+ let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
context 'when a job is protected' do
let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
@@ -324,7 +320,7 @@ module Ci
end
context 'when access_level of runner is ref_protected' do
- let!(:specific_runner) { create(:ci_runner, :ref_protected, :specific) }
+ let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) }
context 'when a job is protected' do
let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index 74a23ed2a3f..ca0c6be5da6 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -6,19 +6,18 @@ describe Ci::UpdateBuildQueueService do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when updating specific runners' do
- let(:runner) { create(:ci_runner) }
+ let(:runner) { create(:ci_runner, :project, projects: [project]) }
context 'when there is a runner that can pick build' do
- before do
- build.project.runners << runner
- end
-
it 'ticks runner queue value' do
expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value }
end
end
context 'when there is no runner that can pick build' do
+ let(:another_project) { create(:project) }
+ let(:runner) { create(:ci_runner, :project, projects: [another_project]) }
+
it 'does not tick runner queue value' do
expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value }
end
@@ -26,7 +25,7 @@ describe Ci::UpdateBuildQueueService do
end
context 'when updating shared runners' do
- let(:runner) { create(:ci_runner, :shared) }
+ let(:runner) { create(:ci_runner, :instance) }
context 'when there is no runner that can pick build' do
it 'ticks runner queue value' do
@@ -56,9 +55,9 @@ describe Ci::UpdateBuildQueueService do
end
context 'when updating group runners' do
- let(:group) { create :group }
- let(:project) { create :project, group: group }
- let(:runner) { create :ci_runner, groups: [group] }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:runner) { create(:ci_runner, :group, groups: [group]) }
context 'when there is a runner that can pick build' do
it 'ticks runner queue value' do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 23b1134b5a3..158541d36e3 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -337,12 +337,18 @@ describe Issues::UpdateService, :mailer do
context 'when the labels change' do
before do
- update_issue(label_ids: [label.id])
+ Timecop.freeze(1.minute.from_now) do
+ update_issue(label_ids: [label.id])
+ end
end
it 'marks todos as done' do
expect(todo.reload.done?).to eq true
end
+
+ it 'updates updated_at' do
+ expect(issue.reload.updated_at).to be > Time.now
+ end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 5279ea6164e..bd2e91f1f7a 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -326,12 +326,18 @@ describe MergeRequests::UpdateService, :mailer do
context 'when the labels change' do
before do
- update_merge_request({ label_ids: [label.id] })
+ Timecop.freeze(1.minute.from_now) do
+ update_merge_request({ label_ids: [label.id] })
+ end
end
it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done
end
+
+ it 'updates updated_at' do
+ expect(merge_request.reload.updated_at).to be > Time.now
+ end
end
context 'when the assignee changes' do
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index 9cf541372b5..5a1dd44bc9d 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -7,7 +7,10 @@ RSpec.configure do |config|
next if example.metadata[:skip_gitaly_mock]
# Use 'and_wrap_original' to make sure the arguments are valid
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) || true }
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original do |m, *args|
+ m.call(*args)
+ !Gitlab::GitalyClient::EXPLICIT_OPT_IN_REQUIRED.include?(args.first)
+ end
end
end
end
diff --git a/spec/support/helpers/drag_to_helper.rb b/spec/support/helpers/drag_to_helper.rb
index ae149631ed9..6d53ad0b602 100644
--- a/spec/support/helpers/drag_to_helper.rb
+++ b/spec/support/helpers/drag_to_helper.rb
@@ -1,6 +1,6 @@
module DragTo
- def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body')
- evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+ def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000)
+ evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), duration: #{duration}, from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
Timeout.timeout(Capybara.default_max_wait_time) do
loop while drag_active?
diff --git a/spec/support/shared_examples/url_validator_examples.rb b/spec/support/shared_examples/url_validator_examples.rb
new file mode 100644
index 00000000000..b4757a70984
--- /dev/null
+++ b/spec/support/shared_examples/url_validator_examples.rb
@@ -0,0 +1,42 @@
+RSpec.shared_examples 'url validator examples' do |protocols|
+ let(:validator) { described_class.new(attributes: [:link_url], **options) }
+ let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
+
+ subject { validator.validate_each(badge, :link_url, badge.link_url) }
+
+ describe '#validates_each' do
+ context 'with no options' do
+ let(:options) { {} }
+
+ it "allows #{protocols.join(',')} protocols by default" do
+ expect(validator.send(:default_options)[:protocols]).to eq protocols
+ end
+
+ it 'checks that the url structure is valid' do
+ badge.link_url = "#{badge.link_url}:invalid_port"
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'with protocols' do
+ let(:options) { { protocols: %w[http] } }
+
+ it 'allows urls with the defined protocols' do
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+
+ it 'add error if the url protocol does not match the selected ones' do
+ badge.link_url = 'https://www.example.com'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/validators/public_url_validator_spec.rb b/spec/validators/public_url_validator_spec.rb
new file mode 100644
index 00000000000..710dd3dc38e
--- /dev/null
+++ b/spec/validators/public_url_validator_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe PublicUrlValidator do
+ include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS
+
+ context 'by default' do
+ let(:validator) { described_class.new(attributes: [:link_url]) }
+ let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
+
+ subject { validator.validate_each(badge, :link_url, badge.link_url) }
+
+ it 'blocks urls pointing to localhost' do
+ badge.link_url = 'https://127.0.0.1'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+
+ it 'blocks urls pointing to the local network' do
+ badge.link_url = 'https://192.168.1.1'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+end
diff --git a/spec/validators/url_placeholder_validator_spec.rb b/spec/validators/url_placeholder_validator_spec.rb
deleted file mode 100644
index b76d8acdf88..00000000000
--- a/spec/validators/url_placeholder_validator_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe UrlPlaceholderValidator do
- let(:validator) { described_class.new(attributes: [:link_url], **options) }
- let!(:badge) { build(:badge) }
- let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
-
- subject { validator.validate_each(badge, :link_url, badge.link_url) }
-
- describe '#validates_each' do
- context 'with no options' do
- let(:options) { {} }
-
- it 'allows http and https protocols by default' do
- expect(validator.send(:default_options)[:protocols]).to eq %w(http https)
- end
-
- it 'checks that the url structure is valid' do
- badge.link_url = placeholder_url
-
- subject
-
- expect(badge.errors.empty?).to be false
- end
- end
-
- context 'with placeholder regex' do
- let(:options) { { placeholder_regex: /(project_path|project_id|commit_sha|default_branch)/ } }
-
- it 'checks that the url is valid and obviate placeholders that match regex' do
- badge.link_url = placeholder_url
-
- subject
-
- expect(badge.errors.empty?).to be true
- end
- end
- end
-end
diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb
index 763dff181d2..2d719263fc8 100644
--- a/spec/validators/url_validator_spec.rb
+++ b/spec/validators/url_validator_spec.rb
@@ -1,46 +1,62 @@
require 'spec_helper'
describe UrlValidator do
- let(:validator) { described_class.new(attributes: [:link_url], **options) }
- let!(:badge) { build(:badge) }
-
+ let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
subject { validator.validate_each(badge, :link_url, badge.link_url) }
- describe '#validates_each' do
- context 'with no options' do
- let(:options) { {} }
+ include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS
+
+ context 'by default' do
+ let(:validator) { described_class.new(attributes: [:link_url]) }
+
+ it 'does not block urls pointing to localhost' do
+ badge.link_url = 'https://127.0.0.1'
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+
+ it 'does not block urls pointing to the local network' do
+ badge.link_url = 'https://192.168.1.1'
- it 'allows http and https protocols by default' do
- expect(validator.send(:default_options)[:protocols]).to eq %w(http https)
- end
+ subject
- it 'checks that the url structure is valid' do
- badge.link_url = 'http://www.google.es/%{whatever}'
+ expect(badge.errors.empty?).to be true
+ end
+ end
+
+ context 'when allow_localhost is set to false' do
+ let(:validator) { described_class.new(attributes: [:link_url], allow_localhost: false) }
+
+ it 'blocks urls pointing to localhost' do
+ badge.link_url = 'https://127.0.0.1'
- subject
+ subject
- expect(badge.errors.empty?).to be false
- end
+ expect(badge.errors.empty?).to be false
end
+ end
- context 'with protocols' do
- let(:options) { { protocols: %w(http) } }
+ context 'when allow_local_network is set to false' do
+ let(:validator) { described_class.new(attributes: [:link_url], allow_local_network: false) }
- it 'allows urls with the defined protocols' do
- badge.link_url = 'http://www.example.com'
+ it 'blocks urls pointing to the local network' do
+ badge.link_url = 'https://192.168.1.1'
- subject
+ subject
- expect(badge.errors.empty?).to be true
- end
+ expect(badge.errors.empty?).to be false
+ end
+ end
- it 'add error if the url protocol does not match the selected ones' do
- badge.link_url = 'https://www.example.com'
+ context 'when ports is set' do
+ let(:validator) { described_class.new(attributes: [:link_url], ports: [443]) }
- subject
+ it 'blocks urls with a different port' do
+ subject
- expect(badge.errors.empty?).to be false
- end
+ expect(badge.errors.empty?).to be false
end
end
end