diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-03-06 11:32:27 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2018-03-06 11:32:27 +0000 |
commit | 8975bd3a66d726e6f5ff85d6c55a707d5c7dceb6 (patch) | |
tree | 04c6e567b8a533d3b814d5e383013571aaa489b6 /spec | |
parent | e4bb25f04bcbd2249da2ef55b1ce6b3df18d42fe (diff) | |
parent | ce12b60e97a9a4518dd99b990e20e55ec8da22bc (diff) | |
download | gitlab-ce-8975bd3a66d726e6f5ff85d6c55a707d5c7dceb6.tar.gz |
[ci skip] Merge branch 'master' into 43770-change-clear-runners-cache-ujs-action-to-an-axios-request
* master: (163 commits)
Resolve "Group Leave action is broken on Groups Dashboard and Homepage"
So that it's consistent with other entries and EE
Fix race condition when previewing docs
Resolve "Enable privileged mode for Runner installed on Kubernetes"
Change column to file_sha256. Add test. Add changelog
Add checksum at runner grape api
Revert logic of calculating checksum
Add post migration for checksum calculation
Add ObjectStorageQueue concern and test
Import use_file method from EE and use it for calculation of checksum
Change column type to binary from string
Add checksum to ci_job_artifacts
Make oauth provider login generic
Don't error out in system hook if user has `nil` datetime columns
Use host URL to build JIRA remote link icon
CI/CD-only projects FE
Resolve "SSH key add text"
Changes after review
Projects and groups badges API
Remove default scope from todos
...
Diffstat (limited to 'spec')
237 files changed, 4976 insertions, 6316 deletions
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index b7257fac608..fb6d82d7de3 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -246,7 +246,7 @@ describe AutocompleteController do expect(json_response.size).to eq(1) expect(json_response.first['id']).to eq authorized_project.id - expect(json_response.first['name_with_namespace']).to eq authorized_project.name_with_namespace + expect(json_response.first['name_with_namespace']).to eq authorized_project.full_name end end end @@ -267,7 +267,7 @@ describe AutocompleteController do expect(json_response.size).to eq(1) expect(json_response.first['id']).to eq authorized_search_project.id - expect(json_response.first['name_with_namespace']).to eq authorized_search_project.name_with_namespace + expect(json_response.first['name_with_namespace']).to eq authorized_search_project.full_name end end end diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb index da54aa9054c..185b6b4ce57 100644 --- a/spec/controllers/groups/labels_controller_spec.rb +++ b/spec/controllers/groups/labels_controller_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Groups::LabelsController do - let(:group) { create(:group) } - let(:user) { create(:user) } + set(:group) { create(:group) } + set(:user) { create(:user) } + set(:project) { create(:project, namespace: group) } before do group.add_owner(user) @@ -10,6 +11,34 @@ describe Groups::LabelsController do sign_in(user) end + describe 'GET #index' do + set(:label_1) { create(:label, project: project, title: 'label_1') } + set(:group_label_1) { create(:group_label, group: group, title: 'group_label_1') } + + it 'returns group and project labels by default' do + get :index, group_id: group, format: :json + + label_ids = json_response.map {|label| label['title']} + expect(label_ids).to match_array([label_1.title, group_label_1.title]) + end + + context 'with ancestor group', :nested_groups do + set(:subgroup) { create(:group, parent: group) } + set(:subgroup_label_1) { create(:group_label, group: subgroup, title: 'subgroup_label_1') } + + before do + subgroup.add_owner(user) + end + + it 'returns ancestor group labels', :nested_groups do + get :index, group_id: subgroup, include_ancestor_groups: true, only_group_labels: true, format: :json + + label_ids = json_response.map {|label| label['title']} + expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title]) + end + end + end + describe 'POST #toggle_subscription' do it 'allows user to toggle subscription on group labels' do label = create(:group_label, group: group) diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 954fc79f57d..15ce418d0d6 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -91,6 +91,12 @@ describe Projects::ClustersController do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('cluster_status') end + + it 'invokes schedule_status_update on each application' do + expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update) + + go + end end describe 'security' do diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb index 7c708a418a7..5516c95d044 100644 --- a/spec/controllers/projects/cycle_analytics_controller_spec.rb +++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb @@ -27,7 +27,7 @@ describe Projects::CycleAnalyticsController do milestone = create(:milestone, project: project, created_at: 5.days.ago) issue.update(milestone: milestone) - create_merge_request_closing_issue(issue) + create_merge_request_closing_issue(user, project, issue) end it 'is false' do diff --git a/spec/factories/badge.rb b/spec/factories/badge.rb new file mode 100644 index 00000000000..b87ece946cb --- /dev/null +++ b/spec/factories/badge.rb @@ -0,0 +1,14 @@ +FactoryBot.define do + trait :base_badge do + link_url { generate(:url) } + image_url { generate(:url) } + end + + factory :project_badge, traits: [:base_badge], class: ProjectBadge do + project + end + + factory :group_badge, aliases: [:badge], traits: [:base_badge], class: GroupBadge do + group + end +end diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index 775fbb3d27b..3deca103578 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -34,5 +34,6 @@ 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 end end diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index 8eb709022ce..caaed4d5246 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -9,4 +9,10 @@ FactoryBot.define do trait :with_file do file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end + + # The uniqueness constraint means we can't use the correct OID for all LFS + # objects, so the test needs to decide which (if any) object gets it + trait :correct_oid do + oid 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75' + end end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index a5f22848031..d5e603baeae 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -173,7 +173,7 @@ feature 'Admin Groups' do visit admin_group_path(group) - expect(page).to have_content(empty_project.name_with_namespace) + expect(page).to have_content(empty_project.full_name) expect(page).to have_content('Projects shared with') end end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index d02ac6c2e2a..6d8350e99f1 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -58,7 +58,7 @@ describe "Admin::Projects" do expect(current_path).to eq admin_project_path(project) expect(page).to have_content(project.path) expect(page).to have_content(project.name) - expect(page).to have_content(project.name_with_namespace) + expect(page).to have_content(project.full_name) expect(page).to have_content(project.creator.name) end end diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 7eeed7da998..8de2e3d199b 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -76,8 +76,8 @@ describe "Admin Runners" do describe 'projects' do it 'contains project names' do - expect(page).to have_content(@project1.name_with_namespace) - expect(page).to have_content(@project2.name_with_namespace) + expect(page).to have_content(@project1.full_name) + expect(page).to have_content(@project2.full_name) end end @@ -89,8 +89,8 @@ describe "Admin Runners" do end it 'contains name of correct project' do - expect(page).to have_content(@project1.name_with_namespace) - expect(page).not_to have_content(@project2.name_with_namespace) + expect(page).to have_content(@project1.full_name) + expect(page).not_to have_content(@project2.full_name) end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 2307ba5985e..8f0a3611052 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -382,7 +382,7 @@ describe "Admin::Users" do describe 'update user identities' do before do - allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated]) + allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated]) end it 'modifies twitter identity' do diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 510677ecf56..ef493db3f11 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -6,7 +6,7 @@ feature 'Cycle Analytics', :js do let(:project) { create(:project, :repository) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:milestone) { create(:milestone, project: project) } - let(:mr) { create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") } + let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") } let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) } context 'as an allowed user' do @@ -41,8 +41,8 @@ feature 'Cycle Analytics', :js do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) project.add_master(user) - create_cycle - deploy_master + @build = create_cycle(user, project, issue, mr, milestone, pipeline) + deploy_master(user, project) sign_in(user) visit project_cycle_analytics_path(project) @@ -117,8 +117,8 @@ feature 'Cycle Analytics', :js do project.add_guest(guest) allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) - create_cycle - deploy_master + create_cycle(user, project, issue, mr, milestone, pipeline) + deploy_master(user, project) sign_in(guest) visit project_cycle_analytics_path(project) @@ -166,16 +166,6 @@ feature 'Cycle Analytics', :js do expect(find('.stage-events')).to have_content("!#{mr.iid}") end - def create_cycle - issue.update(milestone: milestone) - pipeline.run - - @build = create(:ci_build, pipeline: pipeline, status: :success, author: user) - - merge_merge_requests_closing_issue(issue) - ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) - end - def click_stage(stage_name) find('.stage-nav li', text: stage_name).click wait_for_requests diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 54652e2d849..8d1d5a51750 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -74,8 +74,8 @@ RSpec.describe 'Dashboard Issues' do find('.new-project-item-select-button').click page.within('.select2-results') do - expect(page).to have_content(project.name_with_namespace) - expect(page).not_to have_content(project_with_issues_disabled.name_with_namespace) + expect(page).to have_content(project.full_name) + expect(page).not_to have_content(project_with_issues_disabled.full_name) end end @@ -84,8 +84,8 @@ RSpec.describe 'Dashboard Issues' do wait_for_requests - project_path = "/#{project.path_with_namespace}" - project_json = { name: project.name_with_namespace, url: project_path }.to_json + project_path = "/#{project.full_path}" + project_json = { name: project.full_name, url: project_path }.to_json # simulate selection, and prevent overlap by dropdown menu first('.project-item-select', visible: false) diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 744041ac425..c8f3a8449f5 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -28,8 +28,8 @@ feature 'Dashboard Merge Requests' do find('.new-project-item-select-button').click page.within('.select2-results') do - expect(page).to have_content(project.name_with_namespace) - expect(page).not_to have_content(project_with_disabled_merge_requests.name_with_namespace) + expect(page).to have_content(project.full_name) + expect(page).not_to have_content(project_with_disabled_merge_requests.full_name) end end end diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb index 2fc34301d51..7b359b0c651 100644 --- a/spec/features/dashboard/todos/todos_filtering_spec.rb +++ b/spec/features/dashboard/todos/todos_filtering_spec.rb @@ -24,14 +24,14 @@ feature 'Dashboard > User filters todos', :js do it 'filters by project' do click_button 'Project' within '.dropdown-menu-project' do - fill_in 'Search projects', with: project_1.name_with_namespace - click_link project_1.name_with_namespace + fill_in 'Search projects', with: project_1.full_name + click_link project_1.full_name end wait_for_requests - expect(page).to have_content project_1.name_with_namespace - expect(page).not_to have_content project_2.name_with_namespace + expect(page).to have_content project_1.full_name + expect(page).not_to have_content project_2.full_name end context 'Author filter' do diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 076a02150a4..3c01ff345fc 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -73,7 +73,7 @@ feature 'issue move to another project' do wait_for_requests page.within '.js-sidebar-move-issue-block' do - expect(page).to have_content new_project.name_with_namespace + expect(page).to have_content new_project.full_name end end end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index e711a191db2..ea7a97d02a0 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -59,7 +59,6 @@ feature 'Issues > User uses quick actions', :js do it 'does not create a note, and sets the due date accordingly' do write_note("/due 2016-08-28") - expect(page).to have_content '/due 2016-08-28' expect(page).not_to have_content 'Commands applied' issue.reload @@ -99,7 +98,6 @@ feature 'Issues > User uses quick actions', :js do it 'does not create a note, and sets the due date accordingly' do write_note("/remove_due_date") - expect(page).to have_content '/remove_due_date' expect(page).not_to have_content 'Commands applied' issue.reload @@ -147,7 +145,6 @@ feature 'Issues > User uses quick actions', :js do it 'does not create a note, and does not mark the issue as a duplicate' do write_note("/duplicate ##{original_issue.to_reference}") - expect(page).to have_content "/duplicate ##{original_issue.to_reference}" expect(page).not_to have_content 'Commands applied' expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}" diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index 8d1e10b7191..7b2c57aa652 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -22,7 +22,7 @@ feature 'Clusters Applications', :js do scenario 'user is unable to install applications' do page.within('.js-cluster-application-row-helm') do expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') end end end @@ -33,13 +33,13 @@ feature 'Clusters Applications', :js do scenario 'user can install applications' do page.within('.js-cluster-application-row-helm') do expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil - expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') end end context 'when user installs Helm' do before do - allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil) + allow(ClusterInstallAppWorker).to receive(:perform_async) page.within('.js-cluster-application-row-helm') do page.find(:css, '.js-cluster-application-install-button').click @@ -50,18 +50,18 @@ feature 'Clusters Applications', :js do page.within('.js-cluster-application-row-helm') do # FE sends request and gets the response, then the buttons is "Install" expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') Clusters::Cluster.last.application_helm.make_installing! # FE starts polling and update the buttons to "Installing" expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') Clusters::Cluster.last.application_helm.make_installed! expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed') end expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') @@ -71,11 +71,14 @@ feature 'Clusters Applications', :js do context 'when user installs Ingress' do context 'when user installs application: Ingress' do before do - allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil) + allow(ClusterInstallAppWorker).to receive(:perform_async) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) create(:clusters_applications_helm, :installed, cluster: cluster) page.within('.js-cluster-application-row-ingress') do + expect(page).to have_css('.js-cluster-application-install-button:not([disabled])') page.find(:css, '.js-cluster-application-install-button').click end end @@ -83,19 +86,28 @@ feature 'Clusters Applications', :js do it 'he sees status transition' do page.within('.js-cluster-application-row-ingress') do # FE sends request and gets the response, then the buttons is "Install" - expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') + expect(page).to have_css('.js-cluster-application-install-button[disabled]') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') Clusters::Cluster.last.application_ingress.make_installing! # FE starts polling and update the buttons to "Installing" - expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') + expect(page).to have_css('.js-cluster-application-install-button[disabled]') + # The application becomes installed but we keep waiting for external IP address Clusters::Cluster.last.application_ingress.make_installed! - expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed') + expect(page).to have_css('.js-cluster-application-install-button[disabled]') + expect(page).to have_selector('.js-no-ip-message') + expect(page.find('.js-ip-address').value).to eq('?') + + # We receive the external IP address and display + Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100') + + expect(page).not_to have_selector('.js-no-ip-message') + expect(page.find('.js-ip-address').value).to eq('192.168.1.100') end expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 0cc68aff494..12bfcc177c7 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb index d575596937d..1f4eec0a317 100644 --- a/spec/features/projects/members/master_manages_access_requests_spec.rb +++ b/spec/features/projects/members/master_manages_access_requests_spec.rb @@ -25,7 +25,7 @@ feature 'Projects > Members > Master manages access requests' do perform_enqueued_jobs { click_on 'Grant access' } expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email] - expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted" + expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was granted" end scenario 'master can deny access' do @@ -36,7 +36,7 @@ feature 'Projects > Members > Master manages access requests' do perform_enqueued_jobs { click_on 'Deny access' } expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email] - expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied" + expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was denied" end def expect_visible_access_request(project, user) diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 4eb36156812..672d5daa3d8 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -21,7 +21,7 @@ feature 'Projects > Members > User requests access', :js do perform_enqueued_jobs { click_link 'Request Access' } expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email] - expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project" + expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.full_name} project" expect(project.requesters.exists?(user_id: user)).to be_truthy expect(page).to have_content 'Your request for access has been queued for review.' diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index b5104747d00..fd561288091 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -142,7 +142,7 @@ feature 'New project' do context 'from git repository url, "Repo by URL"' do before do - first('.import_git').click + first('.js-import-git-toggle-button').click end it 'does not autocomplete sensitive git repo URL' do diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 3a8e7c05cc4..849d85061df 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -86,7 +86,22 @@ describe 'Pipelines', :js do it 'updates content when tab is clicked' do page.find('.js-pipelines-tab-pending').click wait_for_requests - expect(page).to have_content('No pipelines to show.') + expect(page).to have_content('There are currently no pending pipelines.') + end + end + + context 'navigation links' do + before do + visit project_pipelines_path(project) + wait_for_requests + end + + it 'renders run pipeline link' do + expect(page).to have_link('Run Pipeline') + end + + it 'renders ci lint link' do + expect(page).to have_link('CI Lint') end end @@ -542,7 +557,7 @@ describe 'Pipelines', :js do end it 'has a clear caches button' do - expect(page).to have_link 'Clear runner caches' + expect(page).to have_link 'Clear Runner Caches' end describe 'user clicks the button' do @@ -552,19 +567,31 @@ describe 'Pipelines', :js do end it 'increments jobs_cache_index' do - click_link 'Clear runner caches' + click_link 'Clear Runner Caches' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' end end context 'when project does not have jobs_cache_index' do it 'sets jobs_cache_index to 1' do - click_link 'Clear runner caches' + click_link 'Clear Runner Caches' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' end end end end + + describe 'Empty State' do + let(:project) { create(:project, :repository) } + + before do + visit project_pipelines_path(project) + end + + it 'renders empty state' do + expect(page).to have_content 'Build with confidence' + end + end end context 'when user is not logged in' do @@ -575,7 +602,9 @@ describe 'Pipelines', :js do context 'when project is public' do let(:project) { create(:project, :public, :repository) } - it { expect(page).to have_content 'Build with confidence' } + context 'without pipelines' do + it { expect(page).to have_content 'This project is not currently set up to run pipelines.' } + end end context 'when project is private' do diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index 2709047b8de..0a4f57bcd21 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -39,7 +39,7 @@ describe 'User manages project members' do click_link('Import') end - select(project2.name_with_namespace, from: 'source_project_id') + select(project2.full_name, from: 'source_project_id') click_button('Import') project_member = project.project_members.find_by(user_id: user_mike.id) diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb deleted file mode 100644 index 0c67196f53e..00000000000 --- a/spec/features/projects/tree/create_directory_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'spec_helper' - -feature 'Multi-file editor new directory', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - before do - project.add_master(user) - sign_in(user) - - set_cookie('new_repo', 'true') - - visit project_tree_path(project, :master) - - wait_for_requests - - click_link('Web IDE') - - wait_for_requests - end - - after do - set_cookie('new_repo', 'false') - end - - it 'creates directory in current directory' do - find('.add-to-tree').click - - click_link('New directory') - - page.within('.modal') do - find('.form-control').set('folder name') - - click_button('Create directory') - end - - find('.add-to-tree').click - - click_link('New file') - - page.within('.modal-dialog') do - find('.form-control').set('file name') - - click_button('Create file') - end - - wait_for_requests - - find('.multi-file-commit-panel-collapse-btn').click - - fill_in('commit-message', with: 'commit message ide') - - click_button('Commit') - - expect(page).to have_content('folder name') - end -end diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb deleted file mode 100644 index 85f7318c05d..00000000000 --- a/spec/features/projects/tree/create_file_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' - -feature 'Multi-file editor new file', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - before do - project.add_master(user) - sign_in(user) - - set_cookie('new_repo', 'true') - - visit project_tree_path(project, :master) - - wait_for_requests - - click_link('Web IDE') - - wait_for_requests - end - - after do - set_cookie('new_repo', 'false') - end - - it 'creates file in current directory' do - find('.add-to-tree').click - - click_link('New file') - - page.within('.modal') do - find('.form-control').set('file name') - - click_button('Create file') - end - - wait_for_requests - - find('.multi-file-commit-panel-collapse-btn').click - - fill_in('commit-message', with: 'commit message ide') - - click_button('Commit') - - expect(page).to have_content('file name') - end -end diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb deleted file mode 100644 index f81e8677e92..00000000000 --- a/spec/features/projects/tree/upload_file_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'spec_helper' - -feature 'Multi-file editor upload file', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') } - let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') } - - before do - project.add_master(user) - sign_in(user) - - set_cookie('new_repo', 'true') - - visit project_tree_path(project, :master) - - wait_for_requests - - click_link('Web IDE') - - wait_for_requests - end - - after do - set_cookie('new_repo', 'false') - end - - it 'uploads text file' do - find('.add-to-tree').click - - # make the field visible so capybara can use it - execute_script('document.querySelector("#file-upload").classList.remove("hidden")') - attach_file('file-upload', txt_file) - - find('.add-to-tree').click - - expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt') - expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline)) - end - - it 'uploads image file' do - find('.add-to-tree').click - - # make the field visible so capybara can use it - execute_script('document.querySelector("#file-upload").classList.remove("hidden")') - attach_file('file-upload', img_file) - - find('.add-to-tree').click - - expect(page).to have_selector('.multi-file-tab', text: 'dk.png') - expect(page).not_to have_selector('.monaco-editor') - end -end diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 77212fb105b..9e089c5a6cb 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -35,7 +35,7 @@ describe 'User searches for code' do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: 'rspec') diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb index ef9553f2a91..d6120ff8517 100644 --- a/spec/features/search/user_searches_for_issues_spec.rb +++ b/spec/features/search/user_searches_for_issues_spec.rb @@ -34,7 +34,7 @@ describe 'User searches for issues', :js do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: issue1.title) diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb index 3b6739aecbd..68e2f7a857d 100644 --- a/spec/features/search/user_searches_for_merge_requests_spec.rb +++ b/spec/features/search/user_searches_for_merge_requests_spec.rb @@ -33,7 +33,7 @@ describe 'User searches for merge requests', :js do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: merge_request1.title) diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb index 6e197aee498..fc6cd81eb68 100644 --- a/spec/features/search/user_searches_for_milestones_spec.rb +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -33,7 +33,7 @@ describe 'User searches for milestones', :js do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: milestone1.title) diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 00af625dc86..7934779058f 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -18,7 +18,7 @@ describe 'User searches for wiki pages', :js do find('.js-search-project-dropdown').click page.within('.project-filter') do - click_link(project.name_with_namespace) + click_link(project.full_name) end fill_in('dashboard_search', with: 'content') diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb index aa883c964d2..66afe163447 100644 --- a/spec/features/search/user_uses_search_filters_spec.rb +++ b/spec/features/search/user_uses_search_filters_spec.rb @@ -31,7 +31,7 @@ describe 'User uses search filters', :js do wait_for_requests - expect(page).to have_link(group_project.name_with_namespace) + expect(page).to have_link(group_project.full_name) end end end @@ -43,10 +43,10 @@ describe 'User uses search filters', :js do wait_for_requests - click_link(project.name_with_namespace) + click_link(project.full_name) end - expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace) + expect(find('.js-search-project-dropdown')).to have_content(project.full_name) end end end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index 50ee1656e10..fb65b570dd6 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -1,10 +1,6 @@ require 'spec_helper' feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do - before do - allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) - end - def manage_two_factor_authentication click_on 'Manage two-factor authentication' expect(page).to have_content("Setup new U2F device") diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index abb7631d7d7..45439640ea3 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,9 +10,9 @@ describe IssuesFinder do set(:project3) { create(:project, group: subgroup) } set(:milestone) { create(:milestone, project: project1) } set(:label) { create(:label, project: project2) } - set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago) } - set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') } - set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.from_now) } + set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) } + set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) } + set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) } set(:issue4) { create(:issue, project: project3) } set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) } set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) } @@ -275,12 +275,46 @@ describe IssuesFinder do end context 'through created_before' do - let(:params) { { created_before: issue1.created_at + 1.second } } + let(:params) { { created_before: issue1.created_at } } it 'returns issues created on or before the given date' do expect(issues).to contain_exactly(issue1) end end + + context 'through created_after and created_before' do + let(:params) { { created_after: issue2.created_at, created_before: issue3.created_at } } + + it 'returns issues created between the given dates' do + expect(issues).to contain_exactly(issue2, issue3) + end + end + end + + context 'filtering by updated_at' do + context 'through updated_after' do + let(:params) { { updated_after: issue3.updated_at } } + + it 'returns issues updated on or after the given date' do + expect(issues).to contain_exactly(issue3) + end + end + + context 'through updated_before' do + let(:params) { { updated_before: issue1.updated_at } } + + it 'returns issues updated on or before the given date' do + expect(issues).to contain_exactly(issue1) + end + end + + context 'through updated_after and updated_before' do + let(:params) { { updated_after: issue2.updated_at, updated_before: issue3.updated_at } } + + it 'returns issues updated between the given dates' do + expect(issues).to contain_exactly(issue2, issue3) + end + end end context 'filtering by reaction name' do diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index dc76efea35b..d434c501110 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -89,6 +89,25 @@ describe LabelsFinder do expect(finder.execute).to eq [private_subgroup_label_1] end end + + context 'when including labels from group descendants', :nested_groups do + it 'returns labels from group and its descendants' do + private_group_1.add_developer(user) + private_subgroup_1.add_developer(user) + + finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true) + + expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1] + end + + it 'ignores labels from groups which user can not read' do + private_subgroup_1.add_developer(user) + + finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true) + + expect(finder.execute).to eq [private_subgroup_label_1] + end + end end context 'filtering by project_id' do diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 7917a00fc50..c8a43ddf410 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -109,7 +109,7 @@ describe MergeRequestsFinder do end end - context 'with created_after and created_before params' do + context 'filtering by created_at/updated_at' do let(:new_project) { create(:project, forked_from_project: project1) } let!(:new_merge_request) do @@ -117,15 +117,18 @@ describe MergeRequestsFinder do :simple, author: user, created_at: 1.week.from_now, + updated_at: 1.week.from_now, source_project: new_project, - target_project: project1) + target_project: new_project) end let!(:old_merge_request) do create(:merge_request, :simple, author: user, + source_branch: 'feature_1', created_at: 1.week.ago, + updated_at: 1.week.ago, source_project: new_project, target_project: new_project) end @@ -135,7 +138,7 @@ describe MergeRequestsFinder do end it 'filters by created_after' do - params = { project_id: project1.id, created_after: new_merge_request.created_at } + params = { project_id: new_project.id, created_after: new_merge_request.created_at } merge_requests = described_class.new(user, params).execute @@ -143,12 +146,52 @@ describe MergeRequestsFinder do end it 'filters by created_before' do - params = { project_id: new_project.id, created_before: old_merge_request.created_at + 1.second } + params = { project_id: new_project.id, created_before: old_merge_request.created_at } merge_requests = described_class.new(user, params).execute expect(merge_requests).to contain_exactly(old_merge_request) end + + it 'filters by created_after and created_before' do + params = { + project_id: new_project.id, + created_after: old_merge_request.created_at, + created_before: new_merge_request.created_at + } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) + end + + it 'filters by updated_after' do + params = { project_id: new_project.id, updated_after: new_merge_request.updated_at } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(new_merge_request) + end + + it 'filters by updated_before' do + params = { project_id: new_project.id, updated_before: old_merge_request.updated_at } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(old_merge_request) + end + + it 'filters by updated_after and updated_before' do + params = { + project_id: new_project.id, + updated_after: old_merge_request.updated_at, + updated_before: new_merge_request.updated_at + } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request) + end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 7b43494eea2..f1ae2c7ab65 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -75,6 +75,18 @@ describe NotesFinder do end end + context 'for target type' do + let(:project) { create(:project, :repository) } + let!(:note1) { create :note_on_issue, project: project } + let!(:note2) { create :note_on_commit, project: project } + + it 'finds only notes for the selected type' do + notes = described_class.new(project, user, target_type: 'issue').execute + + expect(notes).to eq([note1]) + end + end + context 'for target' do let(:project) { create(:project, :repository) } let(:note1) { create :note_on_commit, project: project } diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb index 90eb0fe21e4..9747b9402a7 100644 --- a/spec/finders/todos_finder_spec.rb +++ b/spec/finders/todos_finder_spec.rb @@ -2,12 +2,13 @@ require 'spec_helper' describe TodosFinder do describe '#execute' do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:finder) { described_class } + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:finder) { described_class } before do - project.add_developer(user) + group.add_developer(user) end describe '#sort' do @@ -34,17 +35,20 @@ describe TodosFinder do end it "sorts by priority" do + project_2 = create(:project) + label_1 = create(:label, title: 'label_1', project: project, priority: 1) label_2 = create(:label, title: 'label_2', project: project, priority: 2) label_3 = create(:label, title: 'label_3', project: project, priority: 3) + label_1_2 = create(:label, title: 'label_1', project: project_2, priority: 1) issue_1 = create(:issue, title: 'issue_1', project: project) issue_2 = create(:issue, title: 'issue_2', project: project) issue_3 = create(:issue, title: 'issue_3', project: project) issue_4 = create(:issue, title: 'issue_4', project: project) - merge_request_1 = create(:merge_request, source_project: project) + merge_request_1 = create(:merge_request, source_project: project_2) - merge_request_1.labels << label_1 + merge_request_1.labels << label_1_2 # Covers the case where Todo has more than one label issue_3.labels << label_1 @@ -57,15 +61,14 @@ describe TodosFinder do todo_2 = create(:todo, user: user, project: project, target: issue_2) todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago) todo_4 = create(:todo, user: user, project: project, target: issue_1) - todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago) + todo_5 = create(:todo, user: user, project: project_2, target: merge_request_1, created_at: 1.hour.ago) + + project_2.add_developer(user) todos = finder.new(user, { sort: 'priority' }).execute - expect(todos.first).to eq(todo_3) - expect(todos.second).to eq(todo_5) - expect(todos.third).to eq(todo_4) - expect(todos.fourth).to eq(todo_2) - expect(todos.fifth).to eq(todo_1) + puts todos.to_sql + expect(todos).to eq([todo_3, todo_5, todo_4, todo_2, todo_1]) end end end diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index 489d563be2b..d27c12e43f2 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -30,7 +30,8 @@ ] } }, - "status_reason": { "type": ["string", "null"] } + "status_reason": { "type": ["string", "null"] }, + "external_ip": { "type": ["string", "null"] } }, "required" : [ "name", "status" ] } diff --git a/spec/fixtures/emails/update_commands_only_reply.eml b/spec/fixtures/emails/update_commands_only_reply.eml new file mode 100644 index 00000000000..bb0d2b0e03a --- /dev/null +++ b/spec/fixtures/emails/update_commands_only_reply.eml @@ -0,0 +1,38 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +In-Reply-To: <issue_1@localhost> +References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost> +Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +/close + +On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta +<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote: +> +> +> +> eviltrout posted in 'Adventure Time Sux' on Discourse Meta: +> +> --- +> hey guys everyone knows adventure time sucks! +> +> --- +> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3 +> +> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences). +> diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 45ffbeb27a4..4590904c93d 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -12,10 +12,10 @@ describe MembersHelper do let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } } let(:group_member_request) { group.request_access(requester) } - it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" } - it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" } - it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" } - it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" } + it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.full_name} project?" } + it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" } + it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.full_name} project?" } + it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.full_name} project?" } it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" } it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" } it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" } @@ -42,7 +42,7 @@ describe MembersHelper do let(:group) { build_stubbed(:group) } let(:user) { build_stubbed(:user) } - it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" } + it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.full_name}\" project?" } it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" } end end diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb index f55163c26e9..63806ef91f3 100644 --- a/spec/helpers/todos_helper_spec.rb +++ b/spec/helpers/todos_helper_spec.rb @@ -26,8 +26,8 @@ describe TodosHelper do expected_results = [ { 'id' => '', 'text' => 'Any Project' }, - { 'id' => projects.second.id, 'text' => projects.second.name_with_namespace }, - { 'id' => projects.first.id, 'text' => projects.first.name_with_namespace } + { 'id' => projects.second.id, 'text' => projects.second.full_name }, + { 'id' => projects.first.id, 'text' => projects.first.full_name } ] expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results) diff --git a/spec/helpers/u2f_helper_spec.rb b/spec/helpers/u2f_helper_spec.rb deleted file mode 100644 index 0d65b4fe0b8..00000000000 --- a/spec/helpers/u2f_helper_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'spec_helper' - -describe U2fHelper do - describe 'when not on mobile' do - it 'does not inject u2f on chrome 40' do - device = double(mobile?: false) - browser = double(chrome?: true, opera?: false, version: 40, device: device) - allow(helper).to receive(:browser).and_return(browser) - expect(helper.inject_u2f_api?).to eq false - end - - it 'injects u2f on chrome 41' do - device = double(mobile?: false) - browser = double(chrome?: true, opera?: false, version: 41, device: device) - allow(helper).to receive(:browser).and_return(browser) - expect(helper.inject_u2f_api?).to eq true - end - - it 'does not inject u2f on opera 39' do - device = double(mobile?: false) - browser = double(chrome?: false, opera?: true, version: 39, device: device) - allow(helper).to receive(:browser).and_return(browser) - expect(helper.inject_u2f_api?).to eq false - end - - it 'injects u2f on opera 40' do - device = double(mobile?: false) - browser = double(chrome?: false, opera?: true, version: 40, device: device) - allow(helper).to receive(:browser).and_return(browser) - expect(helper.inject_u2f_api?).to eq true - end - end - - describe 'when on mobile' do - it 'does not inject u2f on chrome 41' do - device = double(mobile?: true) - browser = double(chrome?: true, opera?: false, version: 41, device: device) - allow(helper).to receive(:browser).and_return(browser) - expect(helper.inject_u2f_api?).to eq false - end - - it 'does not inject u2f on opera 40' do - device = double(mobile?: true) - browser = double(chrome?: false, opera?: true, version: 40, device: device) - allow(helper).to receive(:browser).and_return(browser) - expect(helper.inject_u2f_api?).to eq false - end - end -end diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 80a598e63bd..13d607a06d2 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -9,8 +9,8 @@ import axios from '~/lib/utils/axios_utils'; import '~/boards/models/assignee'; import eventHub from '~/boards/eventhub'; +import '~/vue_shared/models/label'; import '~/boards/models/list'; -import '~/boards/models/label'; import '~/boards/stores/boards_store'; import boardCard from '~/boards/components/board_card.vue'; import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data'; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 8411f4dd8a6..0cf9e4c9ba1 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -7,8 +7,8 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Cookies from 'js-cookie'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 278155c585e..37088a6421c 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -4,8 +4,8 @@ import Vue from 'vue'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/stores/boards_store'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index dbbe14fe3e0..4a11131b55c 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -3,8 +3,8 @@ /* global ListIssue */ import Vue from 'vue'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 34964b20b05..d9a1d692949 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -6,8 +6,8 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import _ from 'underscore'; +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/services/board_service'; diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index 7eecb58a4c3..e9d77f035e3 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -1,7 +1,7 @@ /* global ListIssue */ +import '~/vue_shared/models/label'; import '~/boards/models/issue'; -import '~/boards/models/label'; import '~/boards/models/list'; import '~/boards/models/assignee'; import '~/boards/stores/modal_store'; diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 15832c38f25..d546543d273 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -38,10 +38,75 @@ describe('Applications', () => { expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined(); }); - /* * / it('renders a row for GitLab Runner', () => { expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined(); }); - /* */ + }); + + describe('Ingress application', () => { + describe('when installed', () => { + describe('with ip address', () => { + it('renders ip address with a clipboard button', () => { + vm = mountComponent(Applications, { + applications: { + ingress: { + title: 'Ingress', + status: 'installed', + externalIp: '0.0.0.0', + }, + helm: { title: 'Helm Tiller' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + }, + }); + + expect( + vm.$el.querySelector('.js-ip-address').value, + ).toEqual('0.0.0.0'); + + expect( + vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'), + ).toEqual('0.0.0.0'); + }); + }); + + describe('without ip address', () => { + it('renders an input text with a question mark and an alert text', () => { + vm = mountComponent(Applications, { + applications: { + ingress: { + title: 'Ingress', + status: 'installed', + }, + helm: { title: 'Helm Tiller' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + }, + }); + + expect( + vm.$el.querySelector('.js-ip-address').value, + ).toEqual('?'); + + expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null); + }); + }); + }); + + describe('before installing', () => { + it('does not render the IP address', () => { + vm = mountComponent(Applications, { + applications: { + helm: { title: 'Helm Tiller' }, + ingress: { title: 'Ingress' }, + runner: { title: 'GitLab Runner' }, + prometheus: { title: 'Prometheus' }, + }, + }); + + expect(vm.$el.textContent).not.toContain('Ingress IP Address'); + expect(vm.$el.querySelector('.js-ip-address')).toBe(null); + }); + }); }); }); diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js index 253b3c45243..6ae7a792329 100644 --- a/spec/javascripts/clusters/services/mock_data.js +++ b/spec/javascripts/clusters/services/mock_data.js @@ -18,6 +18,7 @@ const CLUSTERS_MOCK_DATA = { name: 'ingress', status: APPLICATION_ERROR, status_reason: 'Cannot connect', + external_ip: null, }, { name: 'runner', status: APPLICATION_INSTALLING, diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index 726a4ed30de..8028faf2f02 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -75,6 +75,7 @@ describe('Clusters Store', () => { statusReason: mockResponseData.applications[1].status_reason, requestStatus: null, requestReason: null, + externalIp: null, }, runner: { title: 'GitLab Runner', diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index f1da5f81c0f..756a654765b 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -128,6 +128,24 @@ describe('Filtered Search Visual Tokens', () => { }); }); + describe('getEndpointWithQueryParams', () => { + it('returns `endpoint` string as is when second param `endpointQueryParams` is undefined, null or empty string', () => { + const endpoint = 'foo/bar/labels.json'; + expect(subject.getEndpointWithQueryParams(endpoint)).toBe(endpoint); + expect(subject.getEndpointWithQueryParams(endpoint, null)).toBe(endpoint); + expect(subject.getEndpointWithQueryParams(endpoint, '')).toBe(endpoint); + }); + + it('returns `endpoint` string with values of `endpointQueryParams`', () => { + const endpoint = 'foo/bar/labels.json'; + const singleQueryParams = '{"foo":"true"}'; + const multipleQueryParams = '{"foo":"true","bar":"true"}'; + + expect(subject.getEndpointWithQueryParams(endpoint, singleQueryParams)).toBe(`${endpoint}?foo=true`); + expect(subject.getEndpointWithQueryParams(endpoint, multipleQueryParams)).toBe(`${endpoint}?foo=true&bar=true`); + }); + }); + describe('unselectTokens', () => { it('does nothing when there are no tokens', () => { const beforeHTML = tokensContainer.innerHTML; diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 3adc29262f3..46c7b9f54f2 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -129,7 +129,7 @@ describe('AppComponent', () => { vm.fetchGroups({}); setTimeout(() => { - expect(vm.isLoading).toBeFalsy(); + expect(vm.isLoading).toBe(false); expect($.scrollTo).toHaveBeenCalledWith(0); expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.'); done(); @@ -144,10 +144,10 @@ describe('AppComponent', () => { spyOn(vm, 'updateGroups').and.callThrough(); vm.fetchAllGroups(); - expect(vm.isLoading).toBeTruthy(); + expect(vm.isLoading).toBe(true); expect(vm.fetchGroups).toHaveBeenCalled(); setTimeout(() => { - expect(vm.isLoading).toBeFalsy(); + expect(vm.isLoading).toBe(false); expect(vm.updateGroups).toHaveBeenCalled(); done(); }, 0); @@ -181,7 +181,7 @@ describe('AppComponent', () => { spyOn($, 'scrollTo'); vm.fetchPage(2, null, null, true); - expect(vm.isLoading).toBeTruthy(); + expect(vm.isLoading).toBe(true); expect(vm.fetchGroups).toHaveBeenCalledWith({ page: 2, filterGroupsBy: null, @@ -190,7 +190,7 @@ describe('AppComponent', () => { archived: true, }); setTimeout(() => { - expect(vm.isLoading).toBeFalsy(); + expect(vm.isLoading).toBe(false); expect($.scrollTo).toHaveBeenCalledWith(0); expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); expect(window.history.replaceState).toHaveBeenCalledWith({ @@ -216,7 +216,7 @@ describe('AppComponent', () => { spyOn(vm.store, 'setGroupChildren'); vm.toggleChildren(groupItem); - expect(groupItem.isChildrenLoading).toBeTruthy(); + expect(groupItem.isChildrenLoading).toBe(true); expect(vm.fetchGroups).toHaveBeenCalledWith({ parentId: groupItem.id, }); @@ -232,7 +232,7 @@ describe('AppComponent', () => { vm.toggleChildren(groupItem); expect(vm.fetchGroups).not.toHaveBeenCalled(); - expect(groupItem.isOpen).toBeTruthy(); + expect(groupItem.isOpen).toBe(true); }); it('should collapse group if it is already expanded', () => { @@ -241,16 +241,16 @@ describe('AppComponent', () => { vm.toggleChildren(groupItem); expect(vm.fetchGroups).not.toHaveBeenCalled(); - expect(groupItem.isOpen).toBeFalsy(); + expect(groupItem.isOpen).toBe(false); }); it('should set `isChildrenLoading` back to `false` if load request fails', (done) => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true)); vm.toggleChildren(groupItem); - expect(groupItem.isChildrenLoading).toBeTruthy(); + expect(groupItem.isChildrenLoading).toBe(true); setTimeout(() => { - expect(groupItem.isChildrenLoading).toBeFalsy(); + expect(groupItem.isChildrenLoading).toBe(false); done(); }, 0); }); @@ -268,10 +268,10 @@ describe('AppComponent', () => { it('updates props which show modal confirmation dialog', () => { const group = Object.assign({}, mockParentGroupItem); - expect(vm.updateModal).toBeFalsy(); + expect(vm.showModal).toBe(false); expect(vm.groupLeaveConfirmationMessage).toBe(''); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.updateModal).toBeTruthy(); + expect(vm.showModal).toBe(true); expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); }); }); @@ -280,9 +280,9 @@ describe('AppComponent', () => { it('hides modal confirmation which is shown before leaving the group', () => { const group = Object.assign({}, mockParentGroupItem); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.updateModal).toBeTruthy(); + expect(vm.showModal).toBe(true); vm.hideLeaveGroupModal(); - expect(vm.updateModal).toBeFalsy(); + expect(vm.showModal).toBe(false); }); }); @@ -307,8 +307,8 @@ describe('AppComponent', () => { spyOn($, 'scrollTo'); vm.leaveGroup(); - expect(vm.updateModal).toBeFalsy(); - expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.showModal).toBe(false); + expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); setTimeout(() => { expect($.scrollTo).toHaveBeenCalledWith(0); @@ -325,12 +325,12 @@ describe('AppComponent', () => { spyOn(window, 'Flash'); vm.leaveGroup(); - expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBe(false); done(); }, 0); }); @@ -342,12 +342,12 @@ describe('AppComponent', () => { spyOn(window, 'Flash'); vm.leaveGroup(childGroupItem, groupItem); - expect(vm.targetGroup.isBeingRemoved).toBeTruthy(); + expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath); setTimeout(() => { expect(vm.store.removeGroup).not.toHaveBeenCalled(); expect(window.Flash).toHaveBeenCalledWith(message); - expect(vm.targetGroup.isBeingRemoved).toBeFalsy(); + expect(vm.targetGroup.isBeingRemoved).toBe(false); done(); }, 0); }); @@ -379,10 +379,10 @@ describe('AppComponent', () => { it('should set `isSearchEmpty` prop based on groups count', () => { vm.updateGroups(mockGroups); - expect(vm.isSearchEmpty).toBeFalsy(); + expect(vm.isSearchEmpty).toBe(false); vm.updateGroups([]); - expect(vm.isSearchEmpty).toBeTruthy(); + expect(vm.isSearchEmpty).toBe(true); }); }); }); @@ -473,13 +473,16 @@ describe('AppComponent', () => { }); }); - it('renders modal confirmation dialog', () => { + it('renders modal confirmation dialog', (done) => { vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; - vm.updateModal = true; - const modalDialogEl = vm.$el.querySelector('.modal'); - expect(modalDialogEl).not.toBe(null); - expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); - expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + vm.showModal = true; + Vue.nextTick(() => { + const modalDialogEl = vm.$el.querySelector('.modal'); + expect(modalDialogEl).not.toBe(null); + expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); + expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); + done(); + }); }); }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 49799c31995..27f06573432 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -166,6 +166,21 @@ describe('common_utils', () => { }); }); + describe('objectToQueryString', () => { + it('returns empty string when `param` is undefined, null or empty string', () => { + expect(commonUtils.objectToQueryString()).toBe(''); + expect(commonUtils.objectToQueryString('')).toBe(''); + }); + + it('returns query string with values of `params`', () => { + const singleQueryParams = { foo: true }; + const multipleQueryParams = { foo: true, bar: true }; + + expect(commonUtils.objectToQueryString(singleQueryParams)).toBe('foo=true'); + expect(commonUtils.objectToQueryString(multipleQueryParams)).toBe('foo=true&bar=true'); + }); + }); + describe('buildUrlWithCurrentLocation', () => { it('should build an url with current location and given parameters', () => { expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); diff --git a/spec/javascripts/pipelines/blank_state_spec.js b/spec/javascripts/pipelines/blank_state_spec.js new file mode 100644 index 00000000000..b7a9b60d85c --- /dev/null +++ b/spec/javascripts/pipelines/blank_state_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import component from '~/pipelines/components/blank_state.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Pipelines Blank State', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(component); + + vm = mountComponent(Component, + { + svgPath: 'foo', + message: 'Blank State', + }, + ); + }); + + it('should render svg', () => { + expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo'); + }); + + it('should render message', () => { + expect( + vm.$el.querySelector('h4').textContent.trim(), + ).toEqual('Blank State'); + }); +}); diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js index 97f04844b3a..71f77e5f42e 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/javascripts/pipelines/empty_state_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import emptyStateComp from '~/pipelines/components/empty_state.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Empty State', () => { let component; @@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => { beforeEach(() => { EmptyStateComponent = Vue.extend(emptyStateComp); - component = new EmptyStateComponent({ - propsData: { - helpPagePath: 'foo', - emptyStateSvgPath: 'foo', - }, - }).$mount(); + component = mountComponent(EmptyStateComponent, { + helpPagePath: 'foo', + emptyStateSvgPath: 'foo', + canSetCi: true, + }); + }); + + afterEach(() => { + component.$destroy(); }); it('should render empty state SVG', () => { @@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => { expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect( - component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), - ).toContain('Continous Integration can help catch bugs by running your tests automatically'); + component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), + ).toContain('Continous Integration can help catch bugs by running your tests automatically,'); expect( - component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), - ).toContain('Continuous Deployment can help you deliver code to your product environment'); + component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '), + ).toContain('while Continuous Deployment can help you deliver code to your product environment'); }); it('should render a link with provided help path', () => { - expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo'); - expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); + expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual('foo'); + expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain('Get started with Pipelines'); }); }); diff --git a/spec/javascripts/pipelines/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js deleted file mode 100644 index a402857a4d1..00000000000 --- a/spec/javascripts/pipelines/error_state_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import Vue from 'vue'; -import errorStateComp from '~/pipelines/components/error_state.vue'; - -describe('Pipelines Error State', () => { - let component; - let ErrorStateComponent; - - beforeEach(() => { - ErrorStateComponent = Vue.extend(errorStateComp); - - component = new ErrorStateComponent({ - propsData: { - errorStateSvgPath: 'foo', - }, - }).$mount(); - }); - - it('should render error state SVG', () => { - expect(component.$el.querySelector('.svg-content svg')).toBeDefined(); - }); - - it('should render emtpy state information', () => { - expect( - component.$el.querySelector('h4').textContent, - ).toContain('The API failed to fetch the pipelines'); - }); -}); diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js index 09a0c14d96c..77c5258f74c 100644 --- a/spec/javascripts/pipelines/nav_controls_spec.js +++ b/spec/javascripts/pipelines/nav_controls_spec.js @@ -1,116 +1,68 @@ import Vue from 'vue'; import navControlsComp from '~/pipelines/components/nav_controls.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Pipelines Nav Controls', () => { let NavControlsComponent; + let component; beforeEach(() => { NavControlsComponent = Vue.extend(navControlsComp); }); + afterEach(() => { + component.$destroy(); + }); + it('should render link to create a new pipeline', () => { const mockData = { newPipelinePath: 'foo', - hasCiEnabled: true, - helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: true, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline'); - expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath); + expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline'); + expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(mockData.newPipelinePath); }); - it('should not render link to create pipeline if no permission is provided', () => { + it('should not render link to create pipeline if no path is provided', () => { const mockData = { - newPipelinePath: 'foo', - hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: false, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelector('.btn-create')).toEqual(null); + expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null); }); it('should render link for resetting runner caches', () => { const mockData = { newPipelinePath: 'foo', - hasCiEnabled: true, - helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: false, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches'); - expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath); + expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain('Clear Runner Caches'); + expect(component.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(mockData.resetCachePath); }); it('should render link for CI lint', () => { const mockData = { newPipelinePath: 'foo', - hasCiEnabled: true, - helpPagePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - canCreatePipeline: true, - }; - - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); - - expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint'); - expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath); - }); - - it('should render link to help page when CI is not enabled', () => { - const mockData = { - newPipelinePath: 'foo', - hasCiEnabled: false, - helpPagePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - canCreatePipeline: true, - }; - - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); - - expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); - expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath); - }); - - it('should not render link to help page when CI is enabled', () => { - const mockData = { - newPipelinePath: 'foo', - hasCiEnabled: true, helpPagePath: 'foo', ciLintPath: 'foo', resetCachePath: 'foo', - canCreatePipeline: true, }; - const component = new NavControlsComponent({ - propsData: mockData, - }).$mount(); + component = mountComponent(NavControlsComponent, mockData); - expect(component.$el.querySelector('.btn-info')).toEqual(null); + expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint'); + expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(mockData.ciLintPath); }); }); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index 54d5bfd51e6..84fd0329f08 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -7,36 +7,380 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Pipelines', () => { const jsonFixtureName = 'pipelines/pipelines.json'; - preloadFixtures('static/pipelines.html.raw'); preloadFixtures(jsonFixtureName); let PipelinesComponent; let pipelines; - let component; + let vm; + const paths = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + ciLintPath: '/ci/lint', + resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache', + newPipelinePath: '/twitter/flight/pipelines/new', + }; + + const noPermissions = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + }; beforeEach(() => { - loadFixtures('static/pipelines.html.raw'); pipelines = getJSONFixture(jsonFixtureName); PipelinesComponent = Vue.extend(pipelinesComp); }); afterEach(() => { - component.$destroy(); + vm.$destroy(); + }); + + const pipelinesInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify(pipelines), { + status: 200, + })); + }; + + const emptyStateInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({ + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }), { + status: 200, + })); + }; + + const errorInterceptor = (request, next) => { + next(request.respondWith(JSON.stringify({}), { + status: 500, + })); + }; + + describe('With permission', () => { + describe('With pipelines in main tab', () => { + beforeEach((done) => { + Vue.http.interceptors.push(pipelinesInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('renders Run Pipeline button', () => { + expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath); + }); + + it('renders CI Lint button', () => { + expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath); + }); + + it('renders pipelines table', () => { + expect( + vm.$el.querySelectorAll('.gl-responsive-table-row').length, + ).toEqual(pipelines.pipelines.length + 1); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach((done) => { + Vue.http.interceptors.push(emptyStateInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyStateInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('renders Run Pipeline button', () => { + expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath); + }); + + it('renders CI Lint button', () => { + expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath); + }); + + it('renders tab empty state', () => { + expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach((done) => { + Vue.http.interceptors.push(emptyStateInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: true, + ...paths, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyStateInterceptor, + ); + }); + + it('renders empty state', () => { + expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual('Build with confidence'); + expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(paths.helpPagePath); + }); + + it('does not render tabs nor buttons', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull(); + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + }); + + describe('When API returns error', () => { + beforeEach((done) => { + Vue.http.interceptors.push(errorInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: true, + ...paths, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, errorInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('renders buttons', () => { + expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath); + expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); + expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath); + }); + + it('renders error state', () => { + expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.'); + }); + }); + }); + + describe('Without permission', () => { + describe('With pipelines in main tab', () => { + beforeEach((done) => { + Vue.http.interceptors.push(pipelinesInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: false, + ...noPermissions, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + + it('renders pipelines table', () => { + expect( + vm.$el.querySelectorAll('.gl-responsive-table-row').length, + ).toEqual(pipelines.pipelines.length + 1); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach((done) => { + Vue.http.interceptors.push(emptyStateInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: false, + ...noPermissions, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyStateInterceptor, + ); + }); + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + + it('renders tab empty state', () => { + expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach((done) => { + Vue.http.interceptors.push(emptyStateInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: false, + ...noPermissions, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, emptyStateInterceptor, + ); + }); + + it('renders empty state without button to set CI', () => { + expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual('This project is not currently set up to run pipelines.'); + expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull(); + }); + + it('does not render tabs or buttons', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull(); + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + }); + + describe('When API returns error', () => { + beforeEach((done) => { + Vue.http.interceptors.push(errorInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: false, + canCreatePipeline: true, + ...noPermissions, + }); + + setTimeout(() => { + done(); + }); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, errorInterceptor, + ); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); + }); + + it('does not renders buttons', () => { + expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); + expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); + expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); + }); + + it('renders error state', () => { + expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.'); + }); + }); }); describe('successfull request', () => { describe('with pipelines', () => { - const pipelinesInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(pipelines), { - status: 200, - })); - }; - beforeEach(() => { Vue.http.interceptors.push(pipelinesInterceptor); - component = mountComponent(PipelinesComponent, { + vm = mountComponent(PipelinesComponent, { store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, }); }); @@ -48,9 +392,9 @@ describe('Pipelines', () => { it('should render table', (done) => { setTimeout(() => { - expect(component.$el.querySelector('.table-holder')).toBeDefined(); + expect(vm.$el.querySelector('.table-holder')).toBeDefined(); expect( - component.$el.querySelectorAll('.gl-responsive-table-row').length, + vm.$el.querySelectorAll('.gl-responsive-table-row').length, ).toEqual(pipelines.pipelines.length + 1); done(); }); @@ -59,22 +403,22 @@ describe('Pipelines', () => { it('should render navigation tabs', (done) => { setTimeout(() => { expect( - component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), ).toContain('Pending'); expect( - component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), ).toContain('All'); expect( - component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), ).toContain('Running'); expect( - component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), ).toContain('Finished'); expect( - component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), ).toContain('Branches'); expect( - component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), + vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), ).toContain('Tags'); done(); }); @@ -82,10 +426,10 @@ describe('Pipelines', () => { it('should make an API request when using tabs', (done) => { setTimeout(() => { - spyOn(component, 'updateContent'); - component.$el.querySelector('.js-pipelines-tab-finished').click(); + spyOn(vm, 'updateContent'); + vm.$el.querySelector('.js-pipelines-tab-finished').click(); - expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); + expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); done(); }); }); @@ -93,9 +437,9 @@ describe('Pipelines', () => { describe('with pagination', () => { it('should make an API request when using pagination', (done) => { setTimeout(() => { - spyOn(component, 'updateContent'); + spyOn(vm, 'updateContent'); // Mock pagination - component.store.state.pageInfo = { + vm.store.state.pageInfo = { page: 1, total: 10, perPage: 2, @@ -103,9 +447,9 @@ describe('Pipelines', () => { totalPages: 5, }; - Vue.nextTick(() => { - component.$el.querySelector('.js-next-button a').click(); - expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + vm.$nextTick(() => { + vm.$el.querySelector('.js-next-button a').click(); + expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); done(); }); @@ -113,112 +457,249 @@ describe('Pipelines', () => { }); }); }); + }); - describe('without pipelines', () => { - const emptyInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 200, - })); - }; + describe('methods', () => { + beforeEach(() => { + spyOn(history, 'pushState').and.stub(); + }); - beforeEach(() => { - Vue.http.interceptors.push(emptyInterceptor); - }); + describe('updateContent', () => { + it('should set given parameters', () => { + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + vm.updateContent({ scope: 'finished', page: '4' }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, emptyInterceptor, - ); + expect(vm.page).toEqual('4'); + expect(vm.scope).toEqual('finished'); + expect(vm.requestData.scope).toEqual('finished'); + expect(vm.requestData.page).toEqual('4'); }); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); + spyOn(vm, 'updateContent'); - it('should render empty state', (done) => { - component = new PipelinesComponent({ - propsData: { - store: new Store(), - }, - }).$mount(); + vm.onChangeTab('running'); - setTimeout(() => { - expect(component.$el.querySelector('.empty-state')).not.toBe(null); - done(); + expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, }); + spyOn(vm, 'updateContent'); + + vm.onChangePage(4); + + expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' }); }); }); }); - describe('unsuccessfull request', () => { - const errorInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify([]), { - status: 500, - })); - }; - + describe('computed properties', () => { beforeEach(() => { - Vue.http.interceptors.push(errorInterceptor); + vm = mountComponent(PipelinesComponent, { + store: new Store(), + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }); }); - afterEach(() => { - Vue.http.interceptors = _.without( - Vue.http.interceptors, errorInterceptor, - ); + describe('tabs', () => { + it('returns default tabs', () => { + expect(vm.tabs).toEqual([ + { name: 'All', scope: 'all', count: undefined, isActive: true }, + { name: 'Pending', scope: 'pending', count: undefined, isActive: false }, + { name: 'Running', scope: 'running', count: undefined, isActive: false }, + { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, + { name: 'Branches', scope: 'branches', isActive: false }, + { name: 'Tags', scope: 'tags', isActive: false }, + ]); + }); }); - it('should render error state', (done) => { - component = new PipelinesComponent({ - propsData: { - store: new Store(), - }, - }).$mount(); + describe('emptyTabMessage', () => { + it('returns message with scope', (done) => { + vm.scope = 'pending'; - setTimeout(() => { - expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined(); - done(); + vm.$nextTick(() => { + expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.'); + done(); + }); }); - }); - }); - describe('methods', () => { - beforeEach(() => { - spyOn(history, 'pushState').and.stub(); + it('returns message without scope when scope is `all`', () => { + expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.'); + }); }); - describe('updateContent', () => { - it('should set given parameters', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('stateToRender', () => { + it('returns loading state when the app is loading', () => { + expect(vm.stateToRender).toEqual('loading'); + }); + + it('returns error state when app has error', (done) => { + vm.hasError = true; + vm.isLoading = false; + + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('error'); + done(); + }); + }); + + it('returns table list when app has pipelines', (done) => { + vm.isLoading = false; + vm.hasError = false; + vm.state.pipelines = pipelines.pipelines; + + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('tableList'); + + done(); + }); + }); + + it('returns empty tab when app does not have pipelines but project has pipelines', (done) => { + vm.state.count.all = 10; + vm.isLoading = false; + + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('emptyTab'); + + done(); }); - component.updateContent({ scope: 'finished', page: '4' }); + }); + + it('returns empty tab when project has CI', (done) => { + vm.isLoading = false; + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('emptyTab'); - expect(component.page).toEqual('4'); - expect(component.scope).toEqual('finished'); - expect(component.requestData.scope).toEqual('finished'); - expect(component.requestData.page).toEqual('4'); + done(); + }); + }); + + it('returns empty state when project does not have pipelines nor CI', (done) => { + vm.isLoading = false; + vm.hasGitlabCi = false; + vm.$nextTick(() => { + expect(vm.stateToRender).toEqual('emptyState'); + + done(); + }); }); }); - describe('onChangeTab', () => { - it('should set page to 1', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('shouldRenderTabs', () => { + it('returns true when state is loading & has already made the first request', (done) => { + vm.isLoading = true; + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); }); - spyOn(component, 'updateContent'); + }); - component.onChangeTab('running'); + it('returns true when state is tableList & has already made the first request', (done) => { + vm.isLoading = false; + vm.state.pipelines = pipelines.pipelines; + vm.hasMadeRequest = true; - expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); + }); + }); + + it('returns true when state is error & has already made the first request', (done) => { + vm.isLoading = false; + vm.hasError = true; + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); + }); + }); + + it('returns true when state is empty tab & has already made the first request', (done) => { + vm.isLoading = false; + vm.state.count.all = 10; + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(true); + + done(); + }); + }); + + it('returns false when has not made first request', (done) => { + vm.hasMadeRequest = false; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(false); + + done(); + }); + }); + + it('returns false when state is emtpy state', (done) => { + vm.isLoading = false; + vm.hasMadeRequest = true; + vm.hasGitlabCi = false; + + vm.$nextTick(() => { + expect(vm.shouldRenderTabs).toEqual(false); + + done(); + }); }); }); - describe('onChangePage', () => { - it('should update page and keep scope', () => { - component = mountComponent(PipelinesComponent, { - store: new Store(), + describe('shouldRenderButtons', () => { + it('returns true when it has paths & has made the first request', (done) => { + vm.hasMadeRequest = true; + + vm.$nextTick(() => { + expect(vm.shouldRenderButtons).toEqual(true); + + done(); }); - spyOn(component, 'updateContent'); + }); + + it('returns false when it has not made the first request', (done) => { + vm.hasMadeRequest = false; - component.onChangePage(4); + vm.$nextTick(() => { + expect(vm.shouldRenderButtons).toEqual(false); - expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); + done(); + }); }); }); }); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js deleted file mode 100644 index b509cedbe80..00000000000 --- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file } from '../../helpers'; - -describe('Multi-file editor commit sidebar list collapsed', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(listCollapsed); - - vm = createComponentWithStore(Component, store); - - vm.$store.state.openFiles.push(file('file1'), file('file2')); - vm.$store.state.openFiles[0].tempFile = true; - vm.$store.state.openFiles.forEach((f) => { - Object.assign(f, { - changed: true, - }); - }); - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders added & modified files count', () => { - expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1'); - }); -}); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js deleted file mode 100644 index 6f1a1d874d3..00000000000 --- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import Vue from 'vue'; -import listItem from '~/ide/components/commit_sidebar/list_item.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { file } from '../../helpers'; - -describe('Multi-file editor commit sidebar list item', () => { - let vm; - let f; - - beforeEach(() => { - const Component = Vue.extend(listItem); - - f = file('test-file'); - - vm = mountComponent(Component, { - file: f, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders file path', () => { - expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); - }); - - describe('computed', () => { - describe('iconName', () => { - it('returns modified when not a tempFile', () => { - expect(vm.iconName).toBe('file-modified'); - }); - - it('returns addition when not a tempFile', () => { - f.tempFile = true; - - expect(vm.iconName).toBe('file-addition'); - }); - }); - - describe('iconClass', () => { - it('returns modified when not a tempFile', () => { - expect(vm.iconClass).toContain('multi-file-modified'); - }); - - it('returns addition when not a tempFile', () => { - f.tempFile = true; - - expect(vm.iconClass).toContain('multi-file-addition'); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js deleted file mode 100644 index aeb9de9ace4..00000000000 --- a/spec/javascripts/repo/components/commit_sidebar/list_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file } from '../../helpers'; - -describe('Multi-file editor commit sidebar list', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(commitSidebarList); - - vm = createComponentWithStore(Component, store, { - title: 'Staged', - fileList: [], - }); - - vm.$store.state.rightPanelCollapsed = false; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('empty file list', () => { - it('renders no changes text', () => { - expect(vm.$el.querySelector('.help-block').textContent.trim()).toBe('No changes'); - }); - }); - - describe('with a list of files', () => { - beforeEach((done) => { - const f = file('file name'); - f.changed = true; - vm.fileList.push(f); - - Vue.nextTick(done); - }); - - it('renders list', () => { - expect(vm.$el.querySelectorAll('li').length).toBe(1); - }); - }); - - describe('collapsed', () => { - beforeEach((done) => { - vm.$store.state.rightPanelCollapsed = true; - - Vue.nextTick(done); - }); - - it('hides list', () => { - expect(vm.$el.querySelector('.list-unstyled')).toBeNull(); - expect(vm.$el.querySelector('.help-block')).toBeNull(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/ide_context_bar_spec.js b/spec/javascripts/repo/components/ide_context_bar_spec.js deleted file mode 100644 index 935da259a99..00000000000 --- a/spec/javascripts/repo/components/ide_context_bar_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import ideContextBar from '~/ide/components/ide_context_bar.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; - -describe('Multi-file editor right context bar', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(ideContextBar); - - vm = createComponentWithStore(Component, store); - - vm.$store.state.rightPanelCollapsed = false; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('collapsed', () => { - beforeEach((done) => { - vm.$store.state.rightPanelCollapsed = true; - - Vue.nextTick(done); - }); - - it('adds collapsed class', () => { - expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull(); - }); - - it('shows correct icon', () => { - expect(vm.currentIcon).toBe('angle-double-left'); - }); - }); - - it('clicking toggle collapse button collapses the bar', () => { - spyOn(vm, 'setPanelCollapsedStatus').and.returnValue(Promise.resolve()); - - vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); - - expect(vm.setPanelCollapsedStatus).toHaveBeenCalledWith({ - side: 'right', - collapsed: true, - }); - }); -}); diff --git a/spec/javascripts/repo/components/ide_repo_tree_spec.js b/spec/javascripts/repo/components/ide_repo_tree_spec.js deleted file mode 100644 index e3bbda514da..00000000000 --- a/spec/javascripts/repo/components/ide_repo_tree_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import ideRepoTree from '~/ide/components/ide_repo_tree.vue'; -import { file, resetStore } from '../helpers'; - -describe('IdeRepoTree', () => { - let vm; - - beforeEach(() => { - const IdeRepoTree = Vue.extend(ideRepoTree); - - vm = new IdeRepoTree({ - store, - propsData: { - treeId: 'abcproject/mybranch', - }, - }); - - vm.$store.state.currentBranch = 'master'; - vm.$store.state.isRoot = true; - vm.$store.state.trees['abcproject/mybranch'] = { - tree: [file()], - }; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a sidebar', () => { - const tbody = vm.$el.querySelector('tbody'); - - expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy(); - expect(tbody.querySelector('.repo-file-options')).toBeFalsy(); - expect(tbody.querySelector('.prev-directory')).toBeFalsy(); - expect(tbody.querySelector('.loading-file')).toBeFalsy(); - expect(tbody.querySelector('.file')).toBeTruthy(); - }); - - it('renders 3 loading files if tree is loading', (done) => { - vm.treeId = '123'; - - Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3); - - done(); - }); - }); - - it('renders a prev directory if is not root', (done) => { - vm.$store.state.isRoot = false; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/ide_side_bar_spec.js b/spec/javascripts/repo/components/ide_side_bar_spec.js deleted file mode 100644 index 79c3c8128e8..00000000000 --- a/spec/javascripts/repo/components/ide_side_bar_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import ideSidebar from '~/ide/components/ide_side_bar.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../helpers'; - -describe('IdeSidebar', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(ideSidebar); - - vm = createComponentWithStore(Component, store).$mount(); - - vm.$store.state.leftPanelCollapsed = false; - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a sidebar', () => { - expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull(); - }); - - describe('collapsed', () => { - beforeEach((done) => { - vm.$store.state.leftPanelCollapsed = true; - - Vue.nextTick(done); - }); - - it('adds collapsed class', () => { - expect(vm.$el.classList).toContain('is-collapsed'); - }); - - it('shows correct icon', () => { - expect(vm.currentIcon).toBe('angle-double-right'); - }); - }); -}); diff --git a/spec/javascripts/repo/components/ide_spec.js b/spec/javascripts/repo/components/ide_spec.js deleted file mode 100644 index 18135177b5e..00000000000 --- a/spec/javascripts/repo/components/ide_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import ide from '~/ide/components/ide.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file, resetStore } from '../helpers'; - -describe('ide component', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(ide); - - vm = createComponentWithStore(Component, store, { - emptyStateSvgPath: 'svg', - }).$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('does not render panel right when no files open', () => { - expect(vm.$el.querySelector('.panel-right')).toBeNull(); - }); - - it('renders panel right when files are open', (done) => { - vm.$store.state.trees['abcproject/mybranch'] = { - tree: [file()], - }; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.panel-right')).toBeNull(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js deleted file mode 100644 index 82597fc75e8..00000000000 --- a/spec/javascripts/repo/components/new_branch_form_spec.js +++ /dev/null @@ -1,114 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import newBranchForm from '~/ide/components/new_branch_form.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../helpers'; - -describe('Multi-file editor new branch form', () => { - let vm; - - beforeEach(() => { - const Component = Vue.extend(newBranchForm); - - vm = createComponentWithStore(Component, store); - - vm.$store.state.currentBranch = 'master'; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - describe('template', () => { - it('renders submit as disabled', () => { - expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBe('disabled'); - }); - - it('enables the submit button when branch is not empty', (done) => { - vm.branchName = 'testing'; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBeNull(); - - done(); - }); - }); - - it('displays current branch creating from', (done) => { - Vue.nextTick(() => { - expect(vm.$el.querySelector('p').textContent.replace(/\s+/g, ' ').trim()).toBe('Create from: master'); - - done(); - }); - }); - }); - - describe('submitNewBranch', () => { - beforeEach(() => { - spyOn(vm, 'createNewBranch').and.returnValue(Promise.resolve()); - }); - - it('sets to loading', () => { - vm.submitNewBranch(); - - expect(vm.loading).toBeTruthy(); - }); - - it('hides current flash element', (done) => { - vm.$refs.flashContainer.innerHTML = '<div class="flash-alert"></div>'; - - vm.submitNewBranch(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.flash-alert')).toBeNull(); - - done(); - }); - }); - - it('calls createdNewBranch with branchName', () => { - vm.branchName = 'testing'; - - vm.submitNewBranch(); - - expect(vm.createNewBranch).toHaveBeenCalledWith('testing'); - }); - }); - - describe('submitNewBranch with error', () => { - beforeEach(() => { - spyOn(vm, 'createNewBranch').and.returnValue(Promise.reject({ - json: () => Promise.resolve({ - message: 'error message', - }), - })); - }); - - it('sets loading to false', (done) => { - vm.loading = true; - - vm.submitNewBranch(); - - setTimeout(() => { - expect(vm.loading).toBeFalsy(); - - done(); - }); - }); - - it('creates flash element', (done) => { - vm.submitNewBranch(); - - setTimeout(() => { - expect(vm.$el.querySelector('.flash-alert')).not.toBeNull(); - expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message'); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js deleted file mode 100644 index 4a8e4445e2f..00000000000 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import newDropdown from '~/ide/components/new_dropdown/index.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../../helpers'; - -describe('new dropdown component', () => { - let vm; - - beforeEach(() => { - const component = Vue.extend(newDropdown); - - vm = createComponentWithStore(component, store, { - branch: 'master', - path: '', - }); - - vm.$store.state.currentProjectId = 'abcproject'; - vm.$store.state.path = ''; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders new file, upload and new directory links', () => { - expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file'); - expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file'); - expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory'); - }); - - describe('createNewItem', () => { - it('sets modalType to blob when new file is clicked', () => { - vm.$el.querySelectorAll('a')[0].click(); - - expect(vm.modalType).toBe('blob'); - }); - - it('sets modalType to tree when new directory is clicked', () => { - vm.$el.querySelectorAll('a')[2].click(); - - expect(vm.modalType).toBe('tree'); - }); - - it('opens modal when link is clicked', (done) => { - vm.$el.querySelectorAll('a')[0].click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - - done(); - }); - }); - }); - - describe('hideModal', () => { - beforeAll((done) => { - vm.openModal = true; - Vue.nextTick(done); - }); - - it('closes modal after toggling', (done) => { - vm.hideModal(); - - Vue.nextTick() - .then(() => { - expect(vm.$el.querySelector('.modal')).toBeNull(); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js deleted file mode 100644 index d6a1fdd115c..00000000000 --- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js +++ /dev/null @@ -1,237 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import modal from '~/ide/components/new_dropdown/modal.vue'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file, resetStore } from '../../helpers'; - -describe('new file modal component', () => { - const Component = Vue.extend(modal); - let vm; - let projectTree; - - beforeEach(() => { - spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({ - data: { - id: '123', - }, - })); - - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { - id: '123branch', - }, - }, - })); - - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - })); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - ['tree', 'blob'].forEach((type) => { - describe(type, () => { - beforeEach(() => { - store.state.projects.abcproject = { - web_url: '', - }; - store.state.trees = []; - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - projectTree = store.state.trees['abcproject/mybranch']; - store.state.currentProjectId = 'abcproject'; - - vm = createComponentWithStore(Component, store, { - type, - branchId: 'master', - path: '', - parent: projectTree, - }); - - vm.entryName = 'testing'; - - vm.$mount(); - }); - - it(`sets modal title as ${type}`, () => { - const title = type === 'tree' ? 'directory' : 'file'; - - expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`); - }); - - it(`sets button label as ${type}`, () => { - const title = type === 'tree' ? 'directory' : 'file'; - - expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`); - }); - - it(`sets form label as ${type}`, () => { - const title = type === 'tree' ? 'Directory' : 'File'; - - expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`); - }); - - describe('createEntryInStore', () => { - it('calls createTempEntry', () => { - spyOn(vm, 'createTempEntry'); - - vm.createEntryInStore(); - - expect(vm.createTempEntry).toHaveBeenCalledWith({ - projectId: 'abcproject', - branchId: 'master', - parent: projectTree, - name: 'testing', - type, - }); - }); - - it('sets editMode to true', (done) => { - vm.createEntryInStore(); - - setTimeout(() => { - expect(vm.$store.state.editMode).toBeTruthy(); - - done(); - }); - }); - - it('toggles blob view', (done) => { - vm.createEntryInStore(); - - setTimeout(() => { - expect(vm.$store.state.currentBlobView).toBe('repo-editor'); - - done(); - }); - }); - - it('opens newly created file', (done) => { - if (type === 'blob') { - vm.createEntryInStore(); - - setTimeout(() => { - expect(vm.$store.state.openFiles.length).toBe(1); - expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep'); - - done(); - }); - } else { - done(); - } - }); - - if (type === 'blob') { - it('creates new file', (done) => { - vm.createEntryInStore(); - - setTimeout(() => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('testing'); - expect(baseTree[0].type).toBe('blob'); - expect(baseTree[0].tempFile).toBeTruthy(); - - done(); - }); - }); - - it('does not create temp file when file already exists', (done) => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - baseTree.push(file('testing', '1', type)); - - vm.createEntryInStore(); - - setTimeout(() => { - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('testing'); - expect(baseTree[0].type).toBe('blob'); - expect(baseTree[0].tempFile).toBeFalsy(); - - done(); - }); - }); - } else { - it('creates new tree', () => { - vm.createEntryInStore(); - - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('testing'); - expect(baseTree[0].type).toBe('tree'); - expect(baseTree[0].tempFile).toBeTruthy(); - }); - - it('creates multiple trees when entryName has slashes', () => { - vm.entryName = 'app/test'; - vm.createEntryInStore(); - - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('app'); - }); - - it('creates tree in existing tree', () => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - baseTree.push(file('app', '1', 'tree')); - - vm.entryName = 'app/test'; - vm.createEntryInStore(); - - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('app'); - expect(baseTree[0].tempFile).toBeFalsy(); - expect(baseTree[0].tree[0].tempFile).toBeTruthy(); - expect(baseTree[0].tree[0].name).toBe('test'); - }); - - it('does not create new tree when already exists', () => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - baseTree.push(file('app', '1', 'tree')); - - vm.entryName = 'app'; - vm.createEntryInStore(); - - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe('app'); - expect(baseTree[0].tempFile).toBeFalsy(); - expect(baseTree[0].tree.length).toBe(0); - }); - } - }); - }); - }); - - it('focuses field on mount', () => { - document.body.innerHTML += '<div class="js-test"></div>'; - - vm = createComponentWithStore(Component, store, { - type: 'tree', - projectId: 'abcproject', - branchId: 'master', - path: '', - }).$mount('.js-test'); - - expect(document.activeElement).toBe(vm.$refs.fieldName); - - vm.$el.remove(); - }); -}); diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js deleted file mode 100644 index ee8aab3a252..00000000000 --- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js +++ /dev/null @@ -1,158 +0,0 @@ -import Vue from 'vue'; -import upload from '~/ide/components/new_dropdown/upload.vue'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from '../../helpers'; - -describe('new dropdown upload', () => { - let vm; - let projectTree; - - beforeEach(() => { - spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({ - data: { - id: '123', - }, - })); - - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { - id: '123branch', - }, - }, - })); - - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - })); - - const Component = Vue.extend(upload); - - store.state.projects.abcproject = { - web_url: '', - }; - store.state.currentProjectId = 'abcproject'; - store.state.trees = []; - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - projectTree = store.state.trees['abcproject/mybranch']; - - vm = createComponentWithStore(Component, store, { - branchId: 'master', - path: '', - parent: projectTree, - }); - - vm.entryName = 'testing'; - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - describe('readFile', () => { - beforeEach(() => { - spyOn(FileReader.prototype, 'readAsText'); - spyOn(FileReader.prototype, 'readAsDataURL'); - }); - - it('calls readAsText for text files', () => { - const file = { - type: 'text/html', - }; - - vm.readFile(file); - - expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file); - }); - - it('calls readAsDataURL for non-text files', () => { - const file = { - type: 'images/png', - }; - - vm.readFile(file); - - expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file); - }); - }); - - describe('createFile', () => { - const target = { - result: 'content', - }; - const binaryTarget = { - result: 'base64,base64content', - }; - const file = { - name: 'file', - }; - - it('creates new file', (done) => { - vm.createFile(target, file, true); - - vm.$nextTick(() => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe(file.name); - expect(baseTree[0].content).toBe(target.result); - - done(); - }); - }); - - it('creates new file in path', (done) => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - const tree = { - type: 'tree', - name: 'testing', - path: 'testing', - tree: [], - }; - baseTree.push(tree); - - vm.parent = tree; - vm.createFile(target, file, true); - - vm.$nextTick(() => { - expect(baseTree.length).toBe(1); - expect(baseTree[0].tree[0].name).toBe(file.name); - expect(baseTree[0].tree[0].content).toBe(target.result); - expect(baseTree[0].tree[0].path).toBe(`testing/${file.name}`); - - done(); - }); - }); - - it('splits content on base64 if binary', (done) => { - vm.createFile(binaryTarget, file, false); - - vm.$nextTick(() => { - const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].name).toBe(file.name); - expect(baseTree[0].content).toBe(binaryTarget.result.split('base64,')[1]); - expect(baseTree[0].base64).toBe(true); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js deleted file mode 100644 index 934ada9dec2..00000000000 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import Vue from 'vue'; -import * as urlUtils from '~/lib/utils/url_utility'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import repoCommitSection from '~/ide/components/repo_commit_section.vue'; -import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; -import { file, resetStore } from '../helpers'; - -describe('RepoCommitSection', () => { - let vm; - - function createComponent() { - const RepoCommitSection = Vue.extend(repoCommitSection); - - const comp = new RepoCommitSection({ - store, - }).$mount(); - - comp.$store.state.currentProjectId = 'abcproject'; - comp.$store.state.currentBranchId = 'master'; - comp.$store.state.projects.abcproject = { - web_url: '', - branches: { - master: { - workingReference: '1', - }, - }, - }; - - comp.$store.state.rightPanelCollapsed = false; - comp.$store.state.currentBranch = 'master'; - comp.$store.state.openFiles = [file('file1'), file('file2')]; - comp.$store.state.openFiles.forEach(f => Object.assign(f, { - changed: true, - content: 'testing', - })); - - return comp.$mount(); - } - - beforeEach((done) => { - vm = createComponent(); - - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - })); - - Vue.nextTick(done); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a commit section', () => { - const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')]; - const submitCommit = vm.$el.querySelector('form .btn'); - - expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); - expect(changedFileElements.length).toEqual(2); - - changedFileElements.forEach((changedFile, i) => { - expect(changedFile.textContent.trim()).toEqual(vm.$store.getters.changedFiles[i].path); - }); - - expect(submitCommit.disabled).toBeTruthy(); - expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull(); - }); - - describe('when submitting', () => { - let changedFiles; - - beforeEach(() => { - vm.commitMessage = 'testing'; - changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles)); - - spyOn(service, 'commit').and.returnValue(Promise.resolve({ - data: { - short_id: '1', - stats: {}, - }, - })); - }); - - it('allows you to submit', () => { - expect(vm.$el.querySelector('form .btn').disabled).toBeTruthy(); - }); - - it('submits commit', (done) => { - vm.makeCommit(); - - // Wait for the branch check to finish - getSetTimeoutPromise() - .then(() => Vue.nextTick()) - .then(() => { - const args = service.commit.calls.allArgs()[0]; - const { commit_message, actions, branch: payloadBranch } = args[1]; - - expect(commit_message).toBe('testing'); - expect(actions.length).toEqual(2); - expect(payloadBranch).toEqual('master'); - expect(actions[0].action).toEqual('update'); - expect(actions[1].action).toEqual('update'); - expect(actions[0].content).toEqual(changedFiles[0].content); - expect(actions[1].content).toEqual(changedFiles[1].content); - expect(actions[0].file_path).toEqual(changedFiles[0].path); - expect(actions[1].file_path).toEqual(changedFiles[1].path); - }) - .then(done) - .catch(done.fail); - }); - - it('redirects to MR creation page if start new MR checkbox checked', (done) => { - spyOn(urlUtils, 'visitUrl'); - vm.startNewMR = true; - - vm.makeCommit(); - - getSetTimeoutPromise() - .then(() => Vue.nextTick()) - .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js deleted file mode 100644 index 2895b794506..00000000000 --- a/spec/javascripts/repo/components/repo_edit_button_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoEditButton from '~/ide/components/repo_edit_button.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoEditButton', () => { - let vm; - - beforeEach(() => { - const f = file(); - const RepoEditButton = Vue.extend(repoEditButton); - - vm = new RepoEditButton({ - store, - }); - - f.active = true; - vm.$store.dispatch('setInitialData', { - canCommit: true, - onTopOfBranch: true, - }); - vm.$store.state.openFiles.push(f); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders an edit button', () => { - vm.$mount(); - - expect(vm.$el.querySelector('.btn')).not.toBeNull(); - expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit'); - }); - - it('renders edit button with cancel text', () => { - vm.$store.state.editMode = true; - - vm.$mount(); - - expect(vm.$el.querySelector('.btn')).not.toBeNull(); - expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit'); - }); - - it('toggles edit mode on click', (done) => { - vm.$mount(); - - vm.$el.querySelector('.btn').click(); - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit'); - - done(); - }); - }); - - describe('discardPopupOpen', () => { - beforeEach(() => { - vm.$store.state.discardPopupOpen = true; - vm.$store.state.editMode = true; - vm.$store.state.openFiles[0].changed = true; - - vm.$mount(); - }); - - it('renders popup', () => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - }); - - it('removes all changed files', (done) => { - vm.$el.querySelector('.btn-warning').click(); - - vm.$nextTick(() => { - expect(vm.$store.getters.changedFiles.length).toBe(0); - expect(vm.$el.querySelector('.modal')).toBeNull(); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js deleted file mode 100644 index e7b2ed08acd..00000000000 --- a/spec/javascripts/repo/components/repo_editor_spec.js +++ /dev/null @@ -1,60 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoEditor from '~/ide/components/repo_editor.vue'; -import monacoLoader from '~/ide/monaco_loader'; -import { file, resetStore } from '../helpers'; - -describe('RepoEditor', () => { - let vm; - - beforeEach((done) => { - const f = file(); - const RepoEditor = Vue.extend(repoEditor); - - vm = new RepoEditor({ - store, - }); - - f.active = true; - f.tempFile = true; - vm.$store.state.openFiles.push(f); - vm.$store.getters.activeFile.html = 'testing'; - vm.monaco = true; - - vm.$mount(); - - monacoLoader(['vs/editor/editor.main'], () => { - setTimeout(done, 0); - }); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders an ide container', (done) => { - Vue.nextTick(() => { - expect(vm.shouldHideEditor).toBeFalsy(); - - done(); - }); - }); - - describe('when open file is binary and not raw', () => { - beforeEach((done) => { - vm.$store.getters.activeFile.binary = true; - - Vue.nextTick(done); - }); - - it('does not render the IDE', () => { - expect(vm.shouldHideEditor).toBeTruthy(); - }); - - it('shows activeFile html', () => { - expect(vm.$el.textContent).toContain('testing'); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js deleted file mode 100644 index 115569a9117..00000000000 --- a/spec/javascripts/repo/components/repo_file_buttons_spec.js +++ /dev/null @@ -1,49 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoFileButtons from '~/ide/components/repo_file_buttons.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoFileButtons', () => { - const activeFile = file(); - let vm; - - function createComponent() { - const RepoFileButtons = Vue.extend(repoFileButtons); - - activeFile.rawPath = 'test'; - activeFile.blamePath = 'test'; - activeFile.commitsPath = 'test'; - activeFile.active = true; - store.state.openFiles.push(activeFile); - - return new RepoFileButtons({ - store, - }).$mount(); - } - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => { - vm = createComponent(); - - vm.$nextTick(() => { - const raw = vm.$el.querySelector('.raw'); - const blame = vm.$el.querySelector('.blame'); - const history = vm.$el.querySelector('.history'); - - expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.textContent.trim()).toEqual('Raw'); - expect(blame.href).toMatch(`/${activeFile.blamePath}`); - expect(blame.textContent.trim()).toEqual('Blame'); - expect(history.href).toMatch(`/${activeFile.commitsPath}`); - expect(history.textContent.trim()).toEqual('History'); - expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink'); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js deleted file mode 100644 index 27b55ed1f87..00000000000 --- a/spec/javascripts/repo/components/repo_file_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoFile from '~/ide/components/repo_file.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoFile', () => { - const updated = 'updated'; - let vm; - - function createComponent(propsData) { - const RepoFile = Vue.extend(repoFile); - - return new RepoFile({ - store, - propsData, - }).$mount(); - } - - afterEach(() => { - resetStore(vm.$store); - }); - - it('renders link, icon and name', () => { - const RepoFile = Vue.extend(repoFile); - vm = new RepoFile({ - store, - propsData: { - file: file('t4'), - }, - }); - spyOn(vm, 'timeFormated').and.returnValue(updated); - vm.$mount(); - - const name = vm.$el.querySelector('.repo-file-name'); - - expect(name.href).toMatch(''); - expect(name.textContent.trim()).toEqual(vm.file.name); - }); - - it('does render if hasFiles is true and is loading tree', () => { - vm = createComponent({ - file: file('t1'), - }); - - expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy(); - }); - - it('does not render commit message and datetime if mini', (done) => { - vm = createComponent({ - file: file('t2'), - }); - vm.$store.state.openFiles.push(vm.file); - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.commit-message')).toBeFalsy(); - expect(vm.$el.querySelector('.commit-update')).toBeFalsy(); - - done(); - }); - }); - - it('fires clickFile when the link is clicked', () => { - vm = createComponent({ - file: file('t3'), - }); - - spyOn(vm, 'clickFile'); - - vm.$el.click(); - - expect(vm.clickFile).toHaveBeenCalledWith(vm.file); - }); - - describe('submodule', () => { - let f; - - beforeEach(() => { - f = file('submodule name', '123456789'); - f.type = 'submodule'; - - vm = createComponent({ - file: f, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('renders submodule short ID', () => { - expect(vm.$el.querySelector('.commit-sha').textContent.trim()).toBe('12345678'); - }); - - it('renders ID next to submodule name', () => { - expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678'); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js deleted file mode 100644 index 18366fb89bc..00000000000 --- a/spec/javascripts/repo/components/repo_loading_file_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoLoadingFile from '~/ide/components/repo_loading_file.vue'; -import { resetStore } from '../helpers'; - -describe('RepoLoadingFile', () => { - let vm; - - function createComponent() { - const RepoLoadingFile = Vue.extend(repoLoadingFile); - - return new RepoLoadingFile({ - store, - }).$mount(); - } - - function assertLines(lines) { - lines.forEach((line, n) => { - const index = n + 1; - expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy(); - }); - } - - function assertColumns(columns) { - columns.forEach((column) => { - const container = column.querySelector('.animation-container'); - const lines = [...container.querySelectorAll(':scope > div')]; - - expect(container).toBeTruthy(); - expect(lines.length).toEqual(6); - assertLines(lines); - }); - } - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders 3 columns of animated LoC', () => { - vm = createComponent(); - const columns = [...vm.$el.querySelectorAll('td')]; - - expect(columns.length).toEqual(3); - assertColumns(columns); - }); - - it('renders 1 column of animated LoC if isMini', (done) => { - vm = createComponent(); - vm.$store.state.leftPanelCollapsed = true; - vm.$store.state.openFiles.push('test'); - - vm.$nextTick(() => { - const columns = [...vm.$el.querySelectorAll('td')]; - - expect(columns.length).toEqual(1); - assertColumns(columns); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js deleted file mode 100644 index ff26cab2262..00000000000 --- a/spec/javascripts/repo/components/repo_prev_directory_spec.js +++ /dev/null @@ -1,45 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoPrevDirectory from '~/ide/components/repo_prev_directory.vue'; -import { resetStore } from '../helpers'; - -describe('RepoPrevDirectory', () => { - let vm; - const parentLink = 'parent'; - function createComponent() { - const RepoPrevDirectory = Vue.extend(repoPrevDirectory); - - const comp = new RepoPrevDirectory({ - store, - }); - - comp.$store.state.parentTreeUrl = parentLink; - - return comp.$mount(); - } - - beforeEach(() => { - vm = createComponent(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a prev dir link', () => { - const link = vm.$el.querySelector('a'); - - expect(link.href).toMatch(`/${parentLink}`); - expect(link.textContent).toEqual('...'); - }); - - it('clicking row triggers getTreeData', () => { - spyOn(vm, 'getTreeData'); - - vm.$el.querySelector('td').click(); - - expect(vm.getTreeData).toHaveBeenCalledWith({ endpoint: parentLink }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js deleted file mode 100644 index e90837e4cb2..00000000000 --- a/spec/javascripts/repo/components/repo_preview_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoPreview from '~/ide/components/repo_preview.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoPreview', () => { - let vm; - - function createComponent() { - const f = file(); - const RepoPreview = Vue.extend(repoPreview); - - const comp = new RepoPreview({ - store, - }); - - f.active = true; - f.html = 'test'; - - comp.$store.state.openFiles.push(f); - - return comp.$mount(); - } - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders a div with the activeFile html', () => { - vm = createComponent(); - - expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.innerHTML).toContain('test'); - }); -}); diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js deleted file mode 100644 index 933e8d3a06a..00000000000 --- a/spec/javascripts/repo/components/repo_tab_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoTab from '~/ide/components/repo_tab.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoTab', () => { - let vm; - - function createComponent(propsData) { - const RepoTab = Vue.extend(repoTab); - - return new RepoTab({ - store, - propsData, - }).$mount(); - } - - afterEach(() => { - resetStore(vm.$store); - }); - - it('renders a close link and a name link', () => { - vm = createComponent({ - tab: file(), - }); - vm.$store.state.openFiles.push(vm.tab); - const close = vm.$el.querySelector('.multi-file-tab-close'); - const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`); - - expect(close.querySelector('.fa-times')).toBeTruthy(); - expect(name.textContent.trim()).toEqual(vm.tab.name); - }); - - it('fires clickFile when the link is clicked', () => { - vm = createComponent({ - tab: file(), - }); - - spyOn(vm, 'clickFile'); - - vm.$el.click(); - - expect(vm.clickFile).toHaveBeenCalledWith(vm.tab); - }); - - it('calls closeFile when clicking close button', () => { - vm = createComponent({ - tab: file(), - }); - - spyOn(vm, 'closeFile'); - - vm.$el.querySelector('.multi-file-tab-close').click(); - - expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab }); - }); - - it('renders an fa-circle icon if tab is changed', () => { - const tab = file('changedFile'); - tab.changed = true; - vm = createComponent({ - tab, - }); - - expect(vm.$el.querySelector('.multi-file-tab-close .fa-circle')).not.toBeNull(); - }); - - describe('methods', () => { - describe('closeTab', () => { - it('does not close tab if is changed', (done) => { - const tab = file('closeFile'); - tab.changed = true; - tab.opened = true; - vm = createComponent({ - tab, - }); - vm.$store.state.openFiles.push(tab); - vm.$store.dispatch('setFileActive', tab); - - vm.$el.querySelector('.multi-file-tab-close').click(); - - vm.$nextTick(() => { - expect(tab.opened).toBeTruthy(); - - done(); - }); - }); - - it('closes tab when clicking close btn', (done) => { - const tab = file('lose'); - tab.opened = true; - vm = createComponent({ - tab, - }); - vm.$store.state.openFiles.push(tab); - vm.$store.dispatch('setFileActive', tab); - - vm.$el.querySelector('.multi-file-tab-close').click(); - - vm.$nextTick(() => { - expect(tab.opened).toBeFalsy(); - - done(); - }); - }); - }); - }); -}); diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js deleted file mode 100644 index 2c363364d70..00000000000 --- a/spec/javascripts/repo/components/repo_tabs_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoTabs from '~/ide/components/repo_tabs.vue'; -import { file, resetStore } from '../helpers'; - -describe('RepoTabs', () => { - const openedFiles = [file('open1'), file('open2')]; - let vm; - - function createComponent() { - const RepoTabs = Vue.extend(repoTabs); - - return new RepoTabs({ - store, - }).$mount(); - } - - afterEach(() => { - resetStore(vm.$store); - }); - - it('renders a list of tabs', (done) => { - vm = createComponent(); - openedFiles[0].active = true; - vm.$store.state.openFiles = openedFiles; - - vm.$nextTick(() => { - const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')]; - - expect(tabs.length).toEqual(2); - expect(tabs[0].classList.contains('active')).toBeTruthy(); - expect(tabs[1].classList.contains('active')).toBeFalsy(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js deleted file mode 100644 index ac43d221198..00000000000 --- a/spec/javascripts/repo/helpers.js +++ /dev/null @@ -1,16 +0,0 @@ -import { decorateData } from '~/ide/stores/utils'; -import state from '~/ide/stores/state'; - -export const resetStore = (store) => { - store.replaceState(state()); -}; - -export const file = (name = 'name', id = name, type = '') => decorateData({ - id, - type, - icon: 'icon', - url: 'url', - name, - path: name, - lastCommit: {}, -}); diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js deleted file mode 100644 index af12ca15369..00000000000 --- a/spec/javascripts/repo/lib/common/disposable_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import Disposable from '~/ide/lib/common/disposable'; - -describe('Multi-file editor library disposable class', () => { - let instance; - let disposableClass; - - beforeEach(() => { - instance = new Disposable(); - - disposableClass = { - dispose: jasmine.createSpy('dispose'), - }; - }); - - afterEach(() => { - instance.dispose(); - }); - - describe('add', () => { - it('adds disposable classes', () => { - instance.add(disposableClass); - - expect(instance.disposers.size).toBe(1); - }); - }); - - describe('dispose', () => { - beforeEach(() => { - instance.add(disposableClass); - }); - - it('calls dispose on all cached disposers', () => { - instance.dispose(); - - expect(disposableClass.dispose).toHaveBeenCalled(); - }); - - it('clears cached disposers', () => { - instance.dispose(); - - expect(instance.disposers.size).toBe(0); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js deleted file mode 100644 index 563c2e33834..00000000000 --- a/spec/javascripts/repo/lib/common/model_manager_spec.js +++ /dev/null @@ -1,81 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import ModelManager from '~/ide/lib/common/model_manager'; -import { file } from '../../helpers'; - -describe('Multi-file editor library model manager', () => { - let instance; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - instance = new ModelManager(monaco); - - done(); - }); - }); - - afterEach(() => { - instance.dispose(); - }); - - describe('addModel', () => { - it('caches model', () => { - instance.addModel(file()); - - expect(instance.models.size).toBe(1); - }); - - it('caches model by file path', () => { - instance.addModel(file('path-name')); - - expect(instance.models.keys().next().value).toBe('path-name'); - }); - - it('adds model into disposable', () => { - spyOn(instance.disposable, 'add').and.callThrough(); - - instance.addModel(file()); - - expect(instance.disposable.add).toHaveBeenCalled(); - }); - - it('returns cached model', () => { - spyOn(instance.models, 'get').and.callThrough(); - - instance.addModel(file()); - instance.addModel(file()); - - expect(instance.models.get).toHaveBeenCalled(); - }); - }); - - describe('hasCachedModel', () => { - it('returns false when no models exist', () => { - expect(instance.hasCachedModel('path')).toBeFalsy(); - }); - - it('returns true when model exists', () => { - instance.addModel(file('path-name')); - - expect(instance.hasCachedModel('path-name')).toBeTruthy(); - }); - }); - - describe('dispose', () => { - it('clears cached models', () => { - instance.addModel(file()); - - instance.dispose(); - - expect(instance.models.size).toBe(0); - }); - - it('calls disposable dispose', () => { - spyOn(instance.disposable, 'dispose').and.callThrough(); - - instance.dispose(); - - expect(instance.disposable.dispose).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js deleted file mode 100644 index 878a4a3f3fe..00000000000 --- a/spec/javascripts/repo/lib/common/model_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import Model from '~/ide/lib/common/model'; -import { file } from '../../helpers'; - -describe('Multi-file editor library model', () => { - let model; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - model = new Model(monaco, file('path')); - - done(); - }); - }); - - afterEach(() => { - model.dispose(); - }); - - it('creates original model & new model', () => { - expect(model.originalModel).not.toBeNull(); - expect(model.model).not.toBeNull(); - }); - - describe('path', () => { - it('returns file path', () => { - expect(model.path).toBe('path'); - }); - }); - - describe('getModel', () => { - it('returns model', () => { - expect(model.getModel()).toBe(model.model); - }); - }); - - describe('getOriginalModel', () => { - it('returns original model', () => { - expect(model.getOriginalModel()).toBe(model.originalModel); - }); - }); - - describe('onChange', () => { - it('caches event by path', () => { - model.onChange(() => {}); - - expect(model.events.size).toBe(1); - expect(model.events.keys().next().value).toBe('path'); - }); - - it('calls callback on change', (done) => { - const spy = jasmine.createSpy(); - model.onChange(spy); - - model.getModel().setValue('123'); - - setTimeout(() => { - expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything()); - done(); - }); - }); - }); - - describe('dispose', () => { - it('calls disposable dispose', () => { - spyOn(model.disposable, 'dispose').and.callThrough(); - - model.dispose(); - - expect(model.disposable.dispose).toHaveBeenCalled(); - }); - - it('clears events', () => { - model.onChange(() => {}); - - expect(model.events.size).toBe(1); - - model.dispose(); - - expect(model.events.size).toBe(0); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js deleted file mode 100644 index fea12d74dca..00000000000 --- a/spec/javascripts/repo/lib/decorations/controller_spec.js +++ /dev/null @@ -1,120 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; -import DecorationsController from '~/ide/lib/decorations/controller'; -import Model from '~/ide/lib/common/model'; -import { file } from '../../helpers'; - -describe('Multi-file editor library decorations controller', () => { - let editorInstance; - let controller; - let model; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - editorInstance = editor.create(monaco); - editorInstance.createInstance(document.createElement('div')); - - controller = new DecorationsController(editorInstance); - model = new Model(monaco, file('path')); - - done(); - }); - }); - - afterEach(() => { - model.dispose(); - editorInstance.dispose(); - controller.dispose(); - }); - - describe('getAllDecorationsForModel', () => { - it('returns empty array when no decorations exist for model', () => { - const decorations = controller.getAllDecorationsForModel(model); - - expect(decorations).toEqual([]); - }); - - it('returns decorations by model URL', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - const decorations = controller.getAllDecorationsForModel(model); - - expect(decorations[0]).toEqual({ decoration: 'decorationValue' }); - }); - }); - - describe('addDecorations', () => { - it('caches decorations in a new map', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorations.size).toBe(1); - }); - - it('does not create new cache model', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]); - - expect(controller.decorations.size).toBe(1); - }); - - it('caches decorations by model URL', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorations.size).toBe(1); - expect(controller.decorations.keys().next().value).toBe('path'); - }); - - it('calls decorate method', () => { - spyOn(controller, 'decorate'); - - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorate).toHaveBeenCalled(); - }); - }); - - describe('decorate', () => { - it('sets decorations on editor instance', () => { - spyOn(controller.editor.instance, 'deltaDecorations'); - - controller.decorate(model); - - expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []); - }); - - it('caches decorations', () => { - spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); - - controller.decorate(model); - - expect(controller.editorDecorations.size).toBe(1); - }); - - it('caches decorations by model URL', () => { - spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]); - - controller.decorate(model); - - expect(controller.editorDecorations.keys().next().value).toBe('path'); - }); - }); - - describe('dispose', () => { - it('clears cached decorations', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - controller.dispose(); - - expect(controller.decorations.size).toBe(0); - }); - - it('clears cached editorDecorations', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - controller.dispose(); - - expect(controller.editorDecorations.size).toBe(0); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js deleted file mode 100644 index 1d55c165260..00000000000 --- a/spec/javascripts/repo/lib/diff/controller_spec.js +++ /dev/null @@ -1,176 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; -import ModelManager from '~/ide/lib/common/model_manager'; -import DecorationsController from '~/ide/lib/decorations/controller'; -import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller'; -import { computeDiff } from '~/ide/lib/diff/diff'; -import { file } from '../../helpers'; - -describe('Multi-file editor library dirty diff controller', () => { - let editorInstance; - let controller; - let modelManager; - let decorationsController; - let model; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - editorInstance = editor.create(monaco); - editorInstance.createInstance(document.createElement('div')); - - modelManager = new ModelManager(monaco); - decorationsController = new DecorationsController(editorInstance); - - model = modelManager.addModel(file()); - - controller = new DirtyDiffController(modelManager, decorationsController); - - done(); - }); - }); - - afterEach(() => { - controller.dispose(); - model.dispose(); - decorationsController.dispose(); - editorInstance.dispose(); - }); - - describe('getDiffChangeType', () => { - ['added', 'removed', 'modified'].forEach((type) => { - it(`returns ${type}`, () => { - const change = { - [type]: true, - }; - - expect(getDiffChangeType(change)).toBe(type); - }); - }); - }); - - describe('getDecorator', () => { - ['added', 'removed', 'modified'].forEach((type) => { - it(`returns with linesDecorationsClassName for ${type}`, () => { - const change = { - [type]: true, - }; - - expect( - getDecorator(change).options.linesDecorationsClassName, - ).toBe(`dirty-diff dirty-diff-${type}`); - }); - - it('returns with line numbers', () => { - const change = { - lineNumber: 1, - endLineNumber: 2, - [type]: true, - }; - - const range = getDecorator(change).range; - - expect(range.startLineNumber).toBe(1); - expect(range.endLineNumber).toBe(2); - expect(range.startColumn).toBe(1); - expect(range.endColumn).toBe(1); - }); - }); - }); - - describe('attachModel', () => { - it('adds change event callback', () => { - spyOn(model, 'onChange'); - - controller.attachModel(model); - - expect(model.onChange).toHaveBeenCalled(); - }); - - it('calls throttledComputeDiff on change', () => { - spyOn(controller, 'throttledComputeDiff'); - - controller.attachModel(model); - - model.getModel().setValue('123'); - - expect(controller.throttledComputeDiff).toHaveBeenCalled(); - }); - }); - - describe('computeDiff', () => { - it('posts to worker', () => { - spyOn(controller.dirtyDiffWorker, 'postMessage'); - - controller.computeDiff(model); - - expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({ - path: model.path, - originalContent: '', - newContent: '', - }); - }); - }); - - describe('reDecorate', () => { - it('calls decorations controller decorate', () => { - spyOn(controller.decorationsController, 'decorate'); - - controller.reDecorate(model); - - expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); - }); - }); - - describe('decorate', () => { - it('adds decorations into decorations controller', () => { - spyOn(controller.decorationsController, 'addDecorations'); - - controller.decorate({ data: { changes: [], path: 'path' } }); - - expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything()); - }); - - it('adds decorations into editor', () => { - const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); - - controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } }); - - expect(spy).toHaveBeenCalledWith([], [{ - range: new monaco.Range( - 1, 1, 1, 1, - ), - options: { - isWholeLine: true, - linesDecorationsClassName: 'dirty-diff dirty-diff-modified', - }, - }]); - }); - }); - - describe('dispose', () => { - it('calls disposable dispose', () => { - spyOn(controller.disposable, 'dispose').and.callThrough(); - - controller.dispose(); - - expect(controller.disposable.dispose).toHaveBeenCalled(); - }); - - it('terminates worker', () => { - spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough(); - - controller.dispose(); - - expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled(); - }); - - it('removes worker event listener', () => { - spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough(); - - controller.dispose(); - - expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything()); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js deleted file mode 100644 index 57f3ac3d365..00000000000 --- a/spec/javascripts/repo/lib/diff/diff_spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import { computeDiff } from '~/ide/lib/diff/diff'; - -describe('Multi-file editor library diff calculator', () => { - describe('computeDiff', () => { - it('returns empty array if no changes', () => { - const diff = computeDiff('123', '123'); - - expect(diff).toEqual([]); - }); - - describe('modified', () => { - it('', () => { - const diff = computeDiff('123', '1234')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeUndefined(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(2); - }); - }); - - describe('added', () => { - it('', () => { - const diff = computeDiff('123', '123\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0]; - - expect(diff.added).toBeTruthy(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(3); - }); - }); - - describe('removed', () => { - it('', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeTruthy(); - }); - - it('', () => { - const diff = computeDiff('123\n123\n123', '123\n123')[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBeTruthy(); - expect(diff.removed).toBeTruthy(); - expect(diff.lineNumber).toBe(2); - }); - }); - - it('includes line number of change', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.lineNumber).toBe(1); - }); - - it('includes end line number of change', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.endLineNumber).toBe(1); - }); - }); -}); diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js deleted file mode 100644 index edbf5450dce..00000000000 --- a/spec/javascripts/repo/lib/editor_options_spec.js +++ /dev/null @@ -1,7 +0,0 @@ -import editorOptions from '~/ide/lib/editor_options'; - -describe('Multi-file editor library editor options', () => { - it('returns an array', () => { - expect(editorOptions).toEqual(jasmine.any(Array)); - }); -}); diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js deleted file mode 100644 index 8d51d48a782..00000000000 --- a/spec/javascripts/repo/lib/editor_spec.js +++ /dev/null @@ -1,128 +0,0 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; -import { file } from '../helpers'; - -describe('Multi-file editor library', () => { - let instance; - - beforeEach((done) => { - monacoLoader(['vs/editor/editor.main'], () => { - instance = editor.create(monaco); - - done(); - }); - }); - - afterEach(() => { - instance.dispose(); - }); - - it('creates instance of editor', () => { - expect(editor.editorInstance).not.toBeNull(); - }); - - describe('createInstance', () => { - let el; - - beforeEach(() => { - el = document.createElement('div'); - }); - - it('creates editor instance', () => { - spyOn(instance.monaco.editor, 'create').and.callThrough(); - - instance.createInstance(el); - - expect(instance.monaco.editor.create).toHaveBeenCalled(); - }); - - it('creates dirty diff controller', () => { - instance.createInstance(el); - - expect(instance.dirtyDiffController).not.toBeNull(); - }); - }); - - describe('createModel', () => { - it('calls model manager addModel', () => { - spyOn(instance.modelManager, 'addModel'); - - instance.createModel('FILE'); - - expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE'); - }); - }); - - describe('attachModel', () => { - let model; - - beforeEach(() => { - instance.createInstance(document.createElement('div')); - - model = instance.createModel(file()); - }); - - it('sets the current model on the instance', () => { - instance.attachModel(model); - - expect(instance.currentModel).toBe(model); - }); - - it('attaches the model to the current instance', () => { - spyOn(instance.instance, 'setModel'); - - instance.attachModel(model); - - expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel()); - }); - - it('attaches the model to the dirty diff controller', () => { - spyOn(instance.dirtyDiffController, 'attachModel'); - - instance.attachModel(model); - - expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model); - }); - - it('re-decorates with the dirty diff controller', () => { - spyOn(instance.dirtyDiffController, 'reDecorate'); - - instance.attachModel(model); - - expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model); - }); - }); - - describe('clearEditor', () => { - it('resets the editor model', () => { - instance.createInstance(document.createElement('div')); - - spyOn(instance.instance, 'setModel'); - - instance.clearEditor(); - - expect(instance.instance.setModel).toHaveBeenCalledWith(null); - }); - }); - - describe('dispose', () => { - it('calls disposble dispose method', () => { - spyOn(instance.disposable, 'dispose').and.callThrough(); - - instance.dispose(); - - expect(instance.disposable.dispose).toHaveBeenCalled(); - }); - - it('resets instance', () => { - instance.createInstance(document.createElement('div')); - - expect(instance.instance).not.toBeNull(); - - instance.dispose(); - - expect(instance.instance).toBeNull(); - }); - }); -}); diff --git a/spec/javascripts/repo/monaco_loader_spec.js b/spec/javascripts/repo/monaco_loader_spec.js deleted file mode 100644 index b8ac36972aa..00000000000 --- a/spec/javascripts/repo/monaco_loader_spec.js +++ /dev/null @@ -1,13 +0,0 @@ -import monacoContext from 'monaco-editor/dev/vs/loader'; -import monacoLoader from '~/ide/monaco_loader'; - -describe('MonacoLoader', () => { - it('calls require.config and exports require', () => { - expect(monacoContext.require.getConfig()).toEqual(jasmine.objectContaining({ - paths: { - vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase - }, - })); - expect(monacoLoader).toBe(monacoContext.require); - }); -}); diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js deleted file mode 100644 index 00d16fd790d..00000000000 --- a/spec/javascripts/repo/stores/actions/branch_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { resetStore } from '../../helpers'; - -describe('Multi-file store branch actions', () => { - afterEach(() => { - resetStore(store); - }); - - describe('createNewBranch', () => { - beforeEach(() => { - spyOn(service, 'createBranch').and.returnValue(Promise.resolve({ - json: () => ({ - name: 'testing', - }), - })); - spyOn(history, 'pushState'); - - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'testing'; - store.state.projects.abcproject = { - branches: { - master: { - workingReference: '1', - }, - }, - }; - }); - - it('creates new branch', (done) => { - store.dispatch('createNewBranch', 'master') - .then(() => { - expect(store.state.currentBranchId).toBe('testing'); - expect(service.createBranch).toHaveBeenCalledWith('abcproject', { - branch: 'master', - ref: 'testing', - }); - - done(); - }) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js deleted file mode 100644 index e2d8f002e27..00000000000 --- a/spec/javascripts/repo/stores/actions/file_spec.js +++ /dev/null @@ -1,431 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { file, resetStore } from '../../helpers'; - -describe('Multi-file store file actions', () => { - afterEach(() => { - resetStore(store); - }); - - describe('closeFile', () => { - let localFile; - let getLastCommitDataSpy; - let oldGetLastCommitData; - - beforeEach(() => { - getLastCommitDataSpy = jasmine.createSpy('getLastCommitData'); - oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line - store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line - - localFile = file('testFile'); - localFile.active = true; - localFile.opened = true; - localFile.parentTreeUrl = 'parentTreeUrl'; - - store.state.openFiles.push(localFile); - }); - - afterEach(() => { - store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line - }); - - it('closes open files', (done) => { - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(localFile.opened).toBeFalsy(); - expect(localFile.active).toBeFalsy(); - expect(store.state.openFiles.length).toBe(0); - - done(); - }).catch(done.fail); - }); - - it('does not close file if has changed', (done) => { - localFile.changed = true; - - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(localFile.opened).toBeTruthy(); - expect(localFile.active).toBeTruthy(); - expect(store.state.openFiles.length).toBe(1); - - done(); - }).catch(done.fail); - }); - - it('does not close file if temp file', (done) => { - localFile.tempFile = true; - - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(localFile.opened).toBeTruthy(); - expect(localFile.active).toBeTruthy(); - expect(store.state.openFiles.length).toBe(1); - - done(); - }).catch(done.fail); - }); - - it('force closes a changed file', (done) => { - localFile.changed = true; - - store.dispatch('closeFile', { file: localFile, force: true }) - .then(() => { - expect(localFile.opened).toBeFalsy(); - expect(localFile.active).toBeFalsy(); - expect(store.state.openFiles.length).toBe(0); - - done(); - }).catch(done.fail); - }); - - it('sets next file as active', (done) => { - const f = file('otherfile'); - store.state.openFiles.push(f); - - expect(f.active).toBeFalsy(); - - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(f.active).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('calls getLastCommitData', (done) => { - store.dispatch('closeFile', { file: localFile }) - .then(() => { - expect(getLastCommitDataSpy).toHaveBeenCalled(); - - done(); - }).catch(done.fail); - }); - }); - - describe('setFileActive', () => { - let scrollToTabSpy; - let oldScrollToTab; - - beforeEach(() => { - scrollToTabSpy = jasmine.createSpy('scrollToTab'); - oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line - store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line - }); - - afterEach(() => { - store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line - }); - - it('calls scrollToTab', (done) => { - store.dispatch('setFileActive', file('setThisActive')) - .then(() => { - expect(scrollToTabSpy).toHaveBeenCalled(); - - done(); - }).catch(done.fail); - }); - - it('sets the file active', (done) => { - const localFile = file('activeFile'); - - store.dispatch('setFileActive', localFile) - .then(() => { - expect(localFile.active).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('returns early if file is already active', (done) => { - const localFile = file('earlyActive'); - localFile.active = true; - - store.dispatch('setFileActive', localFile) - .then(() => { - expect(scrollToTabSpy).not.toHaveBeenCalled(); - - done(); - }).catch(done.fail); - }); - - it('sets current active file to not active', (done) => { - const localFile = file('currentActive'); - localFile.active = true; - store.state.openFiles.push(localFile); - - store.dispatch('setFileActive', file('newActive')) - .then(() => { - expect(localFile.active).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - - it('resets location.hash for line highlighting', (done) => { - location.hash = 'test'; - - store.dispatch('setFileActive', file('otherActive')) - .then(() => { - expect(location.hash).not.toBe('test'); - - done(); - }).catch(done.fail); - }); - }); - - describe('getFileData', () => { - let localFile; - - beforeEach(() => { - spyOn(service, 'getFileData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'testing getFileData', - }, - json: () => Promise.resolve({ - blame_path: 'blame_path', - commits_path: 'commits_path', - permalink: 'permalink', - raw_path: 'raw_path', - binary: false, - html: '123', - render_error: '', - }), - })); - - localFile = file('newCreate'); - localFile.url = 'getFileDataURL'; - }); - - afterEach(() => { - store.dispatch('closeFile', { - file: localFile, - force: true, - }); - }); - - it('calls the service', (done) => { - store.dispatch('getFileData', localFile) - .then(() => { - expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL'); - - done(); - }).catch(done.fail); - }); - - it('sets the file data', (done) => { - store.dispatch('getFileData', localFile) - .then(Vue.nextTick) - .then(() => { - expect(localFile.blamePath).toBe('blame_path'); - - done(); - }).catch(done.fail); - }); - - it('sets document title', (done) => { - store.dispatch('getFileData', localFile) - .then(() => { - expect(document.title).toBe('testing getFileData'); - - done(); - }).catch(done.fail); - }); - - it('sets the file as active', (done) => { - store.dispatch('getFileData', localFile) - .then(Vue.nextTick) - .then(() => { - expect(localFile.active).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('adds the file to open files', (done) => { - store.dispatch('getFileData', localFile) - .then(Vue.nextTick) - .then(() => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].name).toBe(localFile.name); - - done(); - }).catch(done.fail); - }); - - it('toggles the file loading', (done) => { - store.dispatch('getFileData', localFile) - .then(() => { - expect(localFile.loading).toBeTruthy(); - - return Vue.nextTick(); - }) - .then(() => { - expect(localFile.loading).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - }); - - describe('getRawFileData', () => { - let tmpFile; - - beforeEach(() => { - spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); - - tmpFile = file('tmpFile'); - }); - - it('calls getRawFileData service method', (done) => { - store.dispatch('getRawFileData', tmpFile) - .then(() => { - expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); - - done(); - }).catch(done.fail); - }); - - it('updates file raw data', (done) => { - store.dispatch('getRawFileData', tmpFile) - .then(() => { - expect(tmpFile.raw).toBe('raw'); - - done(); - }).catch(done.fail); - }); - }); - - describe('changeFileContent', () => { - let tmpFile; - - beforeEach(() => { - tmpFile = file('tmpFile'); - }); - - it('updates file content', (done) => { - store.dispatch('changeFileContent', { - file: tmpFile, - content: 'content', - }) - .then(() => { - expect(tmpFile.content).toBe('content'); - - done(); - }).catch(done.fail); - }); - }); - - describe('createTempFile', () => { - let projectTree; - - beforeEach(() => { - document.body.innerHTML += '<div class="flash-container"></div>'; - - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - branches: { - master: { - workingReference: '1', - }, - }, - }; - - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - - projectTree = store.state.trees['abcproject/mybranch']; - }); - - afterEach(() => { - document.querySelector('.flash-container').remove(); - }); - - it('creates temp file', (done) => { - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then((f) => { - expect(f.tempFile).toBeTruthy(); - expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1); - - done(); - }).catch(done.fail); - }); - - it('adds tmp file to open files', (done) => { - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then((f) => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].name).toBe(f.name); - - done(); - }).catch(done.fail); - }); - - it('sets tmp file as active', (done) => { - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then((f) => { - expect(f.active).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('enters edit mode if file is not base64', (done) => { - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then(() => { - expect(store.state.editMode).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('creates flash message is file already exists', (done) => { - store.state.trees['abcproject/mybranch'].tree.push(file('test', '1', 'blob')); - - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then(() => { - expect(document.querySelector('.flash-alert')).not.toBeNull(); - - done(); - }).catch(done.fail); - }); - - it('increases level of file', (done) => { - store.state.trees['abcproject/mybranch'].level = 1; - - store.dispatch('createTempFile', { - name: 'test', - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - }).then((f) => { - expect(f.level).toBe(2); - - done(); - }).catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js deleted file mode 100644 index 65351dbb7d9..00000000000 --- a/spec/javascripts/repo/stores/actions/tree_spec.js +++ /dev/null @@ -1,350 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { file, resetStore } from '../../helpers'; - -describe('Multi-file store tree actions', () => { - let projectTree; - - const basicCallParameters = { - endpoint: 'rootEndpoint', - projectId: 'abcproject', - branch: 'master', - }; - - beforeEach(() => { - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - web_url: '', - branches: { - master: { - workingReference: '1', - }, - }, - }; - }); - - afterEach(() => { - resetStore(store); - }); - - describe('getTreeData', () => { - beforeEach(() => { - spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - })); - }); - - it('calls service getTreeData', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint'); - - done(); - }).catch(done.fail); - }); - - it('adds data into tree', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - projectTree = store.state.trees['abcproject/master']; - expect(projectTree.tree.length).toBe(3); - expect(projectTree.tree[0].type).toBe('tree'); - expect(projectTree.tree[1].type).toBe('submodule'); - expect(projectTree.tree[2].type).toBe('blob'); - - done(); - }).catch(done.fail); - }); - - it('sets parent tree URL', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(store.state.parentTreeUrl).toBe('parent_tree_url'); - - done(); - }).catch(done.fail); - }); - - it('sets last commit path', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(store.state.trees['abcproject/master'].lastCommitPath).toBe('last_commit_path'); - - done(); - }).catch(done.fail); - }); - - it('sets root if not currently at root', (done) => { - store.state.isInitialRoot = false; - - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(store.state.isInitialRoot).toBeTruthy(); - expect(store.state.isRoot).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('sets page title', (done) => { - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(document.title).toBe('test'); - - done(); - }).catch(done.fail); - }); - - it('calls getLastCommitData if prevLastCommitPath is not null', (done) => { - const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData'); - const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line - store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line - store.state.prevLastCommitPath = 'test'; - - store.dispatch('getTreeData', basicCallParameters) - .then(() => { - expect(getLastCommitDataSpy).toHaveBeenCalledWith(projectTree); - - store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line - - done(); - }).catch(done.fail); - }); - }); - - describe('toggleTreeOpen', () => { - let oldGetTreeData; - let getTreeDataSpy; - let tree; - - beforeEach(() => { - getTreeDataSpy = jasmine.createSpy('getTreeData'); - - oldGetTreeData = store._actions.getTreeData; // eslint-disable-line - store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line - - tree = { - projectId: 'abcproject', - branchId: 'master', - opened: false, - tree: [], - }; - }); - - afterEach(() => { - store._actions.getTreeData = oldGetTreeData; // eslint-disable-line - }); - - it('toggles the tree open', (done) => { - store.dispatch('toggleTreeOpen', { - endpoint: 'test', - tree, - }).then(() => { - expect(tree.opened).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('calls getTreeData if tree is closed', (done) => { - store.dispatch('toggleTreeOpen', { - endpoint: 'test', - tree, - }).then(() => { - expect(getTreeDataSpy).toHaveBeenCalledWith({ - projectId: 'abcproject', - branch: 'master', - endpoint: 'test', - tree, - }); - - done(); - }).catch(done.fail); - }); - - it('resets entries tree', (done) => { - Object.assign(tree, { - opened: true, - tree: ['a'], - }); - - store.dispatch('toggleTreeOpen', { - endpoint: 'test', - tree, - }).then(() => { - expect(tree.tree.length).toBe(0); - - done(); - }).catch(done.fail); - }); - }); - - describe('createTempTree', () => { - beforeEach(() => { - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - projectTree = store.state.trees['abcproject/mybranch']; - }); - - it('creates temp tree', (done) => { - store.dispatch('createTempTree', { - projectId: store.state.currentProjectId, - branchId: store.state.currentBranchId, - name: 'test', - parent: projectTree, - }) - .then(() => { - expect(projectTree.tree[0].name).toBe('test'); - expect(projectTree.tree[0].type).toBe('tree'); - - done(); - }).catch(done.fail); - }); - - it('creates new folder inside another tree', (done) => { - const tree = { - type: 'tree', - name: 'testing', - tree: [], - }; - - projectTree.tree.push(tree); - - store.dispatch('createTempTree', { - projectId: store.state.currentProjectId, - branchId: store.state.currentBranchId, - name: 'testing/test', - parent: projectTree, - }) - .then(() => { - expect(projectTree.tree[0].name).toBe('testing'); - expect(projectTree.tree[0].tree[0].tempFile).toBeTruthy(); - expect(projectTree.tree[0].tree[0].name).toBe('test'); - expect(projectTree.tree[0].tree[0].type).toBe('tree'); - - done(); - }).catch(done.fail); - }); - - it('does not create new tree if already exists', (done) => { - const tree = { - type: 'tree', - name: 'testing', - endpoint: 'test', - tree: [], - }; - - projectTree.tree.push(tree); - - store.dispatch('createTempTree', { - projectId: store.state.currentProjectId, - branchId: store.state.currentBranchId, - name: 'testing/test', - parent: projectTree, - }) - .then(() => { - expect(projectTree.tree[0].name).toBe('testing'); - expect(projectTree.tree[0].tempFile).toBeUndefined(); - - done(); - }).catch(done.fail); - }); - }); - - describe('getLastCommitData', () => { - beforeEach(() => { - spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({ - headers: { - 'more-logs-url': null, - }, - json: () => Promise.resolve([{ - type: 'tree', - file_name: 'testing', - commit: { - message: 'commit message', - authored_date: '123', - }, - }]), - })); - - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - - projectTree = store.state.trees['abcproject/mybranch']; - projectTree.tree.push(file('testing', '1', 'tree')); - projectTree.lastCommitPath = 'lastcommitpath'; - }); - - it('calls service with lastCommitPath', (done) => { - store.dispatch('getLastCommitData', projectTree) - .then(() => { - expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath'); - - done(); - }).catch(done.fail); - }); - - it('updates trees last commit data', (done) => { - store.dispatch('getLastCommitData', projectTree) - .then(Vue.nextTick) - .then(() => { - expect(projectTree.tree[0].lastCommit.message).toBe('commit message'); - - done(); - }).catch(done.fail); - }); - - it('does not update entry if not found', (done) => { - projectTree.tree[0].name = 'a'; - - store.dispatch('getLastCommitData', projectTree) - .then(Vue.nextTick) - .then(() => { - expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message'); - - done(); - }).catch(done.fail); - }); - }); - - describe('updateDirectoryData', () => { - it('adds data into tree', (done) => { - const tree = { - tree: [], - }; - const data = { - trees: [{ name: 'tree' }], - submodules: [{ name: 'submodule' }], - blobs: [{ name: 'blob' }], - }; - - store.dispatch('updateDirectoryData', { - data, - tree, - }).then(() => { - expect(tree.tree[0].name).toBe('tree'); - expect(tree.tree[0].type).toBe('tree'); - expect(tree.tree[1].name).toBe('submodule'); - expect(tree.tree[1].type).toBe('submodule'); - expect(tree.tree[2].name).toBe('blob'); - expect(tree.tree[2].type).toBe('blob'); - - done(); - }).catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js deleted file mode 100644 index f678967b092..00000000000 --- a/spec/javascripts/repo/stores/actions_spec.js +++ /dev/null @@ -1,432 +0,0 @@ -import Vue from 'vue'; -import * as urlUtils from '~/lib/utils/url_utility'; -import store from '~/ide/stores'; -import service from '~/ide/services'; -import { resetStore, file } from '../helpers'; - -describe('Multi-file store actions', () => { - afterEach(() => { - resetStore(store); - }); - - describe('redirectToUrl', () => { - it('calls visitUrl', (done) => { - spyOn(urlUtils, 'visitUrl'); - - store.dispatch('redirectToUrl', 'test') - .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalledWith('test'); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('setInitialData', () => { - it('commits initial data', (done) => { - store.dispatch('setInitialData', { canCommit: true }) - .then(() => { - expect(store.state.canCommit).toBeTruthy(); - done(); - }) - .catch(done.fail); - }); - }); - - describe('closeDiscardPopup', () => { - it('closes the discard popup', (done) => { - store.dispatch('closeDiscardPopup', false) - .then(() => { - expect(store.state.discardPopupOpen).toBeFalsy(); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('discardAllChanges', () => { - beforeEach(() => { - store.state.openFiles.push(file('discardAll')); - store.state.openFiles[0].changed = true; - }); - }); - - describe('closeAllFiles', () => { - beforeEach(() => { - store.state.openFiles.push(file('closeAll')); - store.state.openFiles[0].opened = true; - }); - - it('closes all open files', (done) => { - store.dispatch('closeAllFiles') - .then(() => { - expect(store.state.openFiles.length).toBe(0); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('toggleEditMode', () => { - it('toggles edit mode', (done) => { - store.state.editMode = true; - - store.dispatch('toggleEditMode') - .then(() => { - expect(store.state.editMode).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - - it('sets preview mode', (done) => { - store.state.currentBlobView = 'repo-editor'; - store.state.editMode = true; - - store.dispatch('toggleEditMode') - .then(Vue.nextTick) - .then(() => { - expect(store.state.currentBlobView).toBe('repo-preview'); - - done(); - }).catch(done.fail); - }); - - it('opens discard popup if there are changed files', (done) => { - store.state.editMode = true; - store.state.openFiles.push(file('discardChanges')); - store.state.openFiles[0].changed = true; - - store.dispatch('toggleEditMode') - .then(() => { - expect(store.state.discardPopupOpen).toBeTruthy(); - - done(); - }).catch(done.fail); - }); - - it('can force closed if there are changed files', (done) => { - store.state.editMode = true; - - store.state.openFiles.push(file('forceClose')); - store.state.openFiles[0].changed = true; - - store.dispatch('toggleEditMode', true) - .then(() => { - expect(store.state.discardPopupOpen).toBeFalsy(); - expect(store.state.editMode).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - - it('discards file changes', (done) => { - const f = file('discard'); - store.state.editMode = true; - store.state.openFiles.push(f); - f.changed = true; - - store.dispatch('toggleEditMode', true) - .then(Vue.nextTick) - .then(() => { - expect(f.changed).toBeFalsy(); - - done(); - }).catch(done.fail); - }); - }); - - describe('toggleBlobView', () => { - it('sets edit mode view if in edit mode', (done) => { - store.dispatch('toggleBlobView') - .then(() => { - expect(store.state.currentBlobView).toBe('repo-editor'); - - done(); - }) - .catch(done.fail); - }); - - it('sets preview mode view if not in edit mode', (done) => { - store.state.editMode = false; - - store.dispatch('toggleBlobView') - .then(() => { - expect(store.state.currentBlobView).toBe('repo-preview'); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('checkCommitStatus', () => { - beforeEach(() => { - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - branches: { - master: { - workingReference: '1', - }, - }, - }; - }); - - it('calls service', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '123' }, - }, - })); - - store.dispatch('checkCommitStatus') - .then(() => { - expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); - - done(); - }) - .catch(done.fail); - }); - - it('returns true if current ref does not equal returned ID', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '123' }, - }, - })); - - store.dispatch('checkCommitStatus') - .then((val) => { - expect(val).toBeTruthy(); - - done(); - }) - .catch(done.fail); - }); - - it('returns false if current ref equals returned ID', (done) => { - spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({ - data: { - commit: { id: '1' }, - }, - })); - - store.dispatch('checkCommitStatus') - .then((val) => { - expect(val).toBeFalsy(); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('commitChanges', () => { - let payload; - - beforeEach(() => { - spyOn(window, 'scrollTo'); - - document.body.innerHTML += '<div class="flash-container"></div>'; - - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - web_url: 'webUrl', - branches: { - master: { - workingReference: '1', - }, - }, - }; - - payload = { - branch: 'master', - }; - }); - - afterEach(() => { - document.querySelector('.flash-container').remove(); - }); - - describe('success', () => { - beforeEach(() => { - spyOn(service, 'commit').and.returnValue(Promise.resolve({ - data: { - id: '123456', - short_id: '123', - message: 'test message', - committed_date: 'date', - stats: { - additions: '1', - deletions: '2', - }, - }, - })); - }); - - it('calls service', (done) => { - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - expect(service.commit).toHaveBeenCalledWith('abcproject', payload); - - done(); - }).catch(done.fail); - }); - - it('shows flash notice', (done) => { - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - const alert = document.querySelector('.flash-container'); - - expect(alert.querySelector('.flash-notice')).not.toBeNull(); - expect(alert.textContent.trim()).toBe( - 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', - ); - - done(); - }).catch(done.fail); - }); - - it('adds commit data to changed files', (done) => { - const changedFile = file('changed'); - const f = file('newfile'); - changedFile.changed = true; - - store.state.openFiles.push(changedFile, f); - - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - expect(changedFile.lastCommit.message).toBe('test message'); - expect(f.lastCommit.message).not.toBe('test message'); - - done(); - }).catch(done.fail); - }); - - it('scrolls to top of page', (done) => { - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - expect(window.scrollTo).toHaveBeenCalledWith(0, 0); - - done(); - }).catch(done.fail); - }); - - it('redirects to new merge request page', (done) => { - spyOn(urlUtils, 'visitUrl'); - - store.dispatch('commitChanges', { payload, newMr: true }) - .then(() => { - expect(urlUtils.visitUrl).toHaveBeenCalledWith('webUrl/merge_requests/new?merge_request%5Bsource_branch%5D=master'); - - done(); - }).catch(done.fail); - }); - }); - - describe('failed', () => { - beforeEach(() => { - spyOn(service, 'commit').and.returnValue(Promise.resolve({ - data: { - message: 'failed message', - }, - })); - }); - - it('shows failed message', (done) => { - store.dispatch('commitChanges', { payload, newMr: false }) - .then(() => { - const alert = document.querySelector('.flash-container'); - - expect(alert.textContent.trim()).toBe( - 'failed message', - ); - - done(); - }).catch(done.fail); - }); - }); - }); - - describe('createTempEntry', () => { - beforeEach(() => { - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - store.state.projects.abcproject = { - web_url: '', - }; - }); - - it('creates a temp tree', (done) => { - const projectTree = store.state.trees['abcproject/mybranch']; - - store.dispatch('createTempEntry', { - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - name: 'test', - type: 'tree', - }) - .then(() => { - const baseTree = projectTree.tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].tempFile).toBeTruthy(); - expect(baseTree[0].type).toBe('tree'); - - done(); - }) - .catch(done.fail); - }); - - it('creates temp file', (done) => { - const projectTree = store.state.trees['abcproject/mybranch']; - - store.dispatch('createTempEntry', { - projectId: 'abcproject', - branchId: 'mybranch', - parent: projectTree, - name: 'test', - type: 'blob', - }) - .then(() => { - const baseTree = projectTree.tree; - expect(baseTree.length).toBe(1); - expect(baseTree[0].tempFile).toBeTruthy(); - expect(baseTree[0].type).toBe('blob'); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('popHistoryState', () => { - - }); - - describe('scrollToTab', () => { - it('focuses the current active element', (done) => { - document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>'; - const el = document.querySelector('.repo-tab'); - spyOn(el, 'focus'); - - store.dispatch('scrollToTab') - .then(() => { - setTimeout(() => { - expect(el.focus).toHaveBeenCalled(); - - document.getElementById('tabs').remove(); - - done(); - }); - }) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js deleted file mode 100644 index d0d5934f29a..00000000000 --- a/spec/javascripts/repo/stores/getters_spec.js +++ /dev/null @@ -1,114 +0,0 @@ -import * as getters from '~/ide/stores/getters'; -import state from '~/ide/stores/state'; -import { file } from '../helpers'; - -describe('Multi-file store getters', () => { - let localState; - - beforeEach(() => { - localState = state(); - }); - - describe('changedFiles', () => { - it('returns a list of changed opened files', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('changed')); - localState.openFiles[1].changed = true; - - const changedFiles = getters.changedFiles(localState); - - expect(changedFiles.length).toBe(1); - expect(changedFiles[0].name).toBe('changed'); - }); - }); - - describe('activeFile', () => { - it('returns the current active file', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('active')); - localState.openFiles[1].active = true; - - expect(getters.activeFile(localState).name).toBe('active'); - }); - - it('returns undefined if no active files are found', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('active')); - - expect(getters.activeFile(localState)).toBeNull(); - }); - }); - - describe('activeFileExtension', () => { - it('returns the file extension for the current active file', () => { - localState.openFiles.push(file('active')); - localState.openFiles[0].active = true; - localState.openFiles[0].path = 'test.js'; - - expect(getters.activeFileExtension(localState)).toBe('.js'); - - localState.openFiles[0].path = 'test.es6.js'; - - expect(getters.activeFileExtension(localState)).toBe('.js'); - }); - }); - - describe('canEditFile', () => { - beforeEach(() => { - localState.onTopOfBranch = true; - localState.canCommit = true; - - localState.openFiles.push(file()); - localState.openFiles[0].active = true; - }); - - it('returns true if user can commit and has open files', () => { - expect(getters.canEditFile(localState)).toBeTruthy(); - }); - - it('returns false if user can commit and has no open files', () => { - localState.openFiles = []; - - expect(getters.canEditFile(localState)).toBeFalsy(); - }); - - it('returns false if user can commit and active file is binary', () => { - localState.openFiles[0].binary = true; - - expect(getters.canEditFile(localState)).toBeFalsy(); - }); - - it('returns false if user cant commit', () => { - localState.canCommit = false; - - expect(getters.canEditFile(localState)).toBeFalsy(); - }); - }); - - describe('modifiedFiles', () => { - it('returns a list of modified files', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('changed')); - localState.openFiles[1].changed = true; - - const modifiedFiles = getters.modifiedFiles(localState); - - expect(modifiedFiles.length).toBe(1); - expect(modifiedFiles[0].name).toBe('changed'); - }); - }); - - describe('addedFiles', () => { - it('returns a list of added files', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('added')); - localState.openFiles[1].changed = true; - localState.openFiles[1].tempFile = true; - - const modifiedFiles = getters.addedFiles(localState); - - expect(modifiedFiles.length).toBe(1); - expect(modifiedFiles[0].name).toBe('added'); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js deleted file mode 100644 index a7167537ef2..00000000000 --- a/spec/javascripts/repo/stores/mutations/branch_spec.js +++ /dev/null @@ -1,18 +0,0 @@ -import mutations from '~/ide/stores/mutations/branch'; -import state from '~/ide/stores/state'; - -describe('Multi-file store branch mutations', () => { - let localState; - - beforeEach(() => { - localState = state(); - }); - - describe('SET_CURRENT_BRANCH', () => { - it('sets currentBranch', () => { - mutations.SET_CURRENT_BRANCH(localState, 'master'); - - expect(localState.currentBranchId).toBe('master'); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js deleted file mode 100644 index 6e204ef0404..00000000000 --- a/spec/javascripts/repo/stores/mutations/file_spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import mutations from '~/ide/stores/mutations/file'; -import state from '~/ide/stores/state'; -import { file } from '../../helpers'; - -describe('Multi-file store file mutations', () => { - let localState; - let localFile; - - beforeEach(() => { - localState = state(); - localFile = file(); - }); - - describe('SET_FILE_ACTIVE', () => { - it('sets the file active', () => { - mutations.SET_FILE_ACTIVE(localState, { - file: localFile, - active: true, - }); - - expect(localFile.active).toBeTruthy(); - }); - }); - - describe('TOGGLE_FILE_OPEN', () => { - beforeEach(() => { - mutations.TOGGLE_FILE_OPEN(localState, localFile); - }); - - it('adds into opened files', () => { - expect(localFile.opened).toBeTruthy(); - expect(localState.openFiles.length).toBe(1); - }); - - it('removes from opened files', () => { - mutations.TOGGLE_FILE_OPEN(localState, localFile); - - expect(localFile.opened).toBeFalsy(); - expect(localState.openFiles.length).toBe(0); - }); - }); - - describe('SET_FILE_DATA', () => { - it('sets extra file data', () => { - mutations.SET_FILE_DATA(localState, { - data: { - blame_path: 'blame', - commits_path: 'commits', - permalink: 'permalink', - raw_path: 'raw', - binary: true, - html: 'html', - render_error: 'render_error', - }, - file: localFile, - }); - - expect(localFile.blamePath).toBe('blame'); - expect(localFile.commitsPath).toBe('commits'); - expect(localFile.permalink).toBe('permalink'); - expect(localFile.rawPath).toBe('raw'); - expect(localFile.binary).toBeTruthy(); - expect(localFile.html).toBe('html'); - expect(localFile.renderError).toBe('render_error'); - }); - }); - - describe('SET_FILE_RAW_DATA', () => { - it('sets raw data', () => { - mutations.SET_FILE_RAW_DATA(localState, { - file: localFile, - raw: 'testing', - }); - - expect(localFile.raw).toBe('testing'); - }); - }); - - describe('UPDATE_FILE_CONTENT', () => { - beforeEach(() => { - localFile.raw = 'test'; - }); - - it('sets content', () => { - mutations.UPDATE_FILE_CONTENT(localState, { - file: localFile, - content: 'test', - }); - - expect(localFile.content).toBe('test'); - }); - - it('sets changed if content does not match raw', () => { - mutations.UPDATE_FILE_CONTENT(localState, { - file: localFile, - content: 'testing', - }); - - expect(localFile.content).toBe('testing'); - expect(localFile.changed).toBeTruthy(); - }); - }); - - describe('DISCARD_FILE_CHANGES', () => { - beforeEach(() => { - localFile.content = 'test'; - localFile.changed = true; - }); - - it('resets content and changed', () => { - mutations.DISCARD_FILE_CHANGES(localState, localFile); - - expect(localFile.content).toBe(''); - expect(localFile.changed).toBeFalsy(); - }); - }); - - describe('CREATE_TMP_FILE', () => { - it('adds file into parent tree', () => { - const f = file('tmpFile'); - - mutations.CREATE_TMP_FILE(localState, { - file: f, - parent: localFile, - }); - - expect(localFile.tree.length).toBe(1); - expect(localFile.tree[0].name).toBe(f.name); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js deleted file mode 100644 index e6ca8ea139e..00000000000 --- a/spec/javascripts/repo/stores/mutations/tree_spec.js +++ /dev/null @@ -1,71 +0,0 @@ -import mutations from '~/ide/stores/mutations/tree'; -import state from '~/ide/stores/state'; -import { file } from '../../helpers'; - -describe('Multi-file store tree mutations', () => { - let localState; - let localTree; - - beforeEach(() => { - localState = state(); - localTree = file(); - }); - - describe('TOGGLE_TREE_OPEN', () => { - it('toggles tree open', () => { - mutations.TOGGLE_TREE_OPEN(localState, localTree); - - expect(localTree.opened).toBeTruthy(); - - mutations.TOGGLE_TREE_OPEN(localState, localTree); - - expect(localTree.opened).toBeFalsy(); - }); - }); - - describe('SET_DIRECTORY_DATA', () => { - const data = [{ - name: 'tree', - }, - { - name: 'submodule', - }, - { - name: 'blob', - }]; - - it('adds directory data', () => { - mutations.SET_DIRECTORY_DATA(localState, { - data, - tree: localState, - }); - - expect(localState.tree.length).toBe(3); - expect(localState.tree[0].name).toBe('tree'); - expect(localState.tree[1].name).toBe('submodule'); - expect(localState.tree[2].name).toBe('blob'); - }); - }); - - describe('SET_PARENT_TREE_URL', () => { - it('sets the parent tree url', () => { - mutations.SET_PARENT_TREE_URL(localState, 'test'); - - expect(localState.parentTreeUrl).toBe('test'); - }); - }); - - describe('CREATE_TMP_TREE', () => { - it('adds tree into parent tree', () => { - const tmpEntry = file('tmpTree'); - - mutations.CREATE_TMP_TREE(localState, { - tmpEntry, - parent: localTree, - }); - - expect(localTree.tree.length).toBe(1); - expect(localTree.tree[0].name).toBe(tmpEntry.name); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js deleted file mode 100644 index 5fd8ad94972..00000000000 --- a/spec/javascripts/repo/stores/mutations_spec.js +++ /dev/null @@ -1,125 +0,0 @@ -import mutations from '~/ide/stores/mutations'; -import state from '~/ide/stores/state'; -import { file } from '../helpers'; - -describe('Multi-file store mutations', () => { - let localState; - let entry; - - beforeEach(() => { - localState = state(); - entry = file(); - }); - - describe('SET_INITIAL_DATA', () => { - it('sets all initial data', () => { - mutations.SET_INITIAL_DATA(localState, { - test: 'test', - }); - - expect(localState.test).toBe('test'); - }); - }); - - describe('SET_PREVIEW_MODE', () => { - it('sets currentBlobView to repo-preview', () => { - mutations.SET_PREVIEW_MODE(localState); - - expect(localState.currentBlobView).toBe('repo-preview'); - - localState.currentBlobView = 'testing'; - - mutations.SET_PREVIEW_MODE(localState); - - expect(localState.currentBlobView).toBe('repo-preview'); - }); - }); - - describe('SET_EDIT_MODE', () => { - it('sets currentBlobView to repo-editor', () => { - mutations.SET_EDIT_MODE(localState); - - expect(localState.currentBlobView).toBe('repo-editor'); - - localState.currentBlobView = 'testing'; - - mutations.SET_EDIT_MODE(localState); - - expect(localState.currentBlobView).toBe('repo-editor'); - }); - }); - - describe('TOGGLE_LOADING', () => { - it('toggles loading of entry', () => { - mutations.TOGGLE_LOADING(localState, entry); - - expect(entry.loading).toBeTruthy(); - - mutations.TOGGLE_LOADING(localState, entry); - - expect(entry.loading).toBeFalsy(); - }); - }); - - describe('TOGGLE_EDIT_MODE', () => { - it('toggles editMode', () => { - mutations.TOGGLE_EDIT_MODE(localState); - - expect(localState.editMode).toBeFalsy(); - - mutations.TOGGLE_EDIT_MODE(localState); - - expect(localState.editMode).toBeTruthy(); - }); - }); - - describe('TOGGLE_DISCARD_POPUP', () => { - it('sets discardPopupOpen', () => { - mutations.TOGGLE_DISCARD_POPUP(localState, true); - - expect(localState.discardPopupOpen).toBeTruthy(); - - mutations.TOGGLE_DISCARD_POPUP(localState, false); - - expect(localState.discardPopupOpen).toBeFalsy(); - }); - }); - - describe('SET_ROOT', () => { - it('sets isRoot & initialRoot', () => { - mutations.SET_ROOT(localState, true); - - expect(localState.isRoot).toBeTruthy(); - expect(localState.isInitialRoot).toBeTruthy(); - - mutations.SET_ROOT(localState, false); - - expect(localState.isRoot).toBeFalsy(); - expect(localState.isInitialRoot).toBeFalsy(); - }); - }); - - describe('SET_LEFT_PANEL_COLLAPSED', () => { - it('sets left panel collapsed', () => { - mutations.SET_LEFT_PANEL_COLLAPSED(localState, true); - - expect(localState.leftPanelCollapsed).toBeTruthy(); - - mutations.SET_LEFT_PANEL_COLLAPSED(localState, false); - - expect(localState.leftPanelCollapsed).toBeFalsy(); - }); - }); - - describe('SET_RIGHT_PANEL_COLLAPSED', () => { - it('sets right panel collapsed', () => { - mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true); - - expect(localState.rightPanelCollapsed).toBeTruthy(); - - mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false); - - expect(localState.rightPanelCollapsed).toBeFalsy(); - }); - }); -}); diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js deleted file mode 100644 index 89745a2029e..00000000000 --- a/spec/javascripts/repo/stores/utils_spec.js +++ /dev/null @@ -1,119 +0,0 @@ -import * as utils from '~/ide/stores/utils'; -import state from '~/ide/stores/state'; -import { file } from '../helpers'; - -describe('Multi-file store utils', () => { - describe('setPageTitle', () => { - it('sets the document page title', () => { - utils.setPageTitle('test'); - - expect(document.title).toBe('test'); - }); - }); - - describe('treeList', () => { - let localState; - - beforeEach(() => { - localState = state(); - }); - - it('returns flat tree list', () => { - localState.trees = []; - localState.trees['abcproject/mybranch'] = { - tree: [], - }; - const baseTree = localState.trees['abcproject/mybranch'].tree; - baseTree.push(file('1')); - baseTree[0].tree.push(file('2')); - baseTree[0].tree[0].tree.push(file('3')); - - const treeList = utils.treeList(localState, 'abcproject/mybranch'); - - expect(treeList.length).toBe(3); - expect(treeList[1].name).toBe(baseTree[0].tree[0].name); - expect(treeList[2].name).toBe(baseTree[0].tree[0].tree[0].name); - }); - }); - - describe('createTemp', () => { - it('creates temp tree', () => { - const tmp = utils.createTemp({ - name: 'test', - path: 'test', - type: 'tree', - level: 0, - changed: false, - content: '', - base64: '', - }); - - expect(tmp.tempFile).toBeTruthy(); - expect(tmp.icon).toBe('fa-folder'); - }); - - it('creates temp file', () => { - const tmp = utils.createTemp({ - name: 'test', - path: 'test', - type: 'blob', - level: 0, - changed: false, - content: '', - base64: '', - }); - - expect(tmp.tempFile).toBeTruthy(); - expect(tmp.icon).toBe('fa-file-text-o'); - }); - }); - - describe('findIndexOfFile', () => { - let localState; - - beforeEach(() => { - localState = [{ - path: '1', - }, { - path: '2', - }]; - }); - - it('finds in the index of an entry by path', () => { - const index = utils.findIndexOfFile(localState, { - path: '2', - }); - - expect(index).toBe(1); - }); - }); - - describe('findEntry', () => { - let localState; - - beforeEach(() => { - localState = { - tree: [{ - type: 'tree', - name: 'test', - }, { - type: 'blob', - name: 'file', - }], - }; - }); - - it('returns an entry found by name', () => { - const foundEntry = utils.findEntry(localState.tree, 'tree', 'test'); - - expect(foundEntry.type).toBe('tree'); - expect(foundEntry.name).toBe('test'); - }); - - it('returns undefined when no entry found', () => { - const foundEntry = utils.findEntry(localState.tree, 'blob', 'test'); - - expect(foundEntry).toBeUndefined(); - }); - }); -}); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index fb4946aeeea..1bcfdfe72b6 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -37,6 +37,7 @@ window.$ = window.jQuery = $; window.gl = window.gl || {}; window.gl.TEST_HOST = 'http://test.host'; window.gon = window.gon || {}; +window.gon.test_env = true; let hasUnhandledPromiseRejections = false; diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 29b15f3a782..4d15bcc4956 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -5,7 +5,7 @@ import MockU2FDevice from './mock_u2f_device'; describe('U2FAuthenticate', () => { preloadFixtures('u2f/authenticate.html.raw'); - beforeEach(() => { + beforeEach((done) => { loadFixtures('u2f/authenticate.html.raw'); this.u2fDevice = new MockU2FDevice(); this.container = $('#js-authenticate-u2f'); @@ -22,7 +22,7 @@ describe('U2FAuthenticate', () => { // bypass automatic form submission within renderAuthenticated spyOn(this.component, 'renderAuthenticated').and.returnValue(true); - return this.component.start(); + this.component.start().then(done).catch(done.fail); }); it('allows authenticating via a U2F device', () => { @@ -34,7 +34,7 @@ describe('U2FAuthenticate', () => { expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); - return describe('errors', () => { + describe('errors', () => { it('displays an error message', () => { const setupButton = this.container.find('#js-login-u2f-device'); setupButton.trigger('click'); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index b0051f11362..dbe89c2923c 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -5,12 +5,12 @@ import MockU2FDevice from './mock_u2f_device'; describe('U2FRegister', () => { preloadFixtures('u2f/register.html.raw'); - beforeEach(() => { + beforeEach((done) => { loadFixtures('u2f/register.html.raw'); this.u2fDevice = new MockU2FDevice(); this.container = $('#js-register-u2f'); this.component = new U2FRegister(this.container, $('#js-register-u2f-templates'), {}, 'token'); - return this.component.start(); + this.component.start().then(done).catch(done.fail); }); it('allows registering a U2F device', () => { diff --git a/spec/javascripts/u2f/util_spec.js b/spec/javascripts/u2f/util_spec.js new file mode 100644 index 00000000000..4187183236f --- /dev/null +++ b/spec/javascripts/u2f/util_spec.js @@ -0,0 +1,45 @@ +import { canInjectU2fApi } from '~/u2f/util'; + +describe('U2F Utils', () => { + describe('canInjectU2fApi', () => { + it('returns false for Chrome < 41', () => { + const userAgent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.28 Safari/537.36'; + expect(canInjectU2fApi(userAgent)).toBe(false); + }); + + it('returns true for Chrome >= 41', () => { + const userAgent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'; + expect(canInjectU2fApi(userAgent)).toBe(true); + }); + + it('returns false for Opera < 40', () => { + const userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.25'; + expect(canInjectU2fApi(userAgent)).toBe(false); + }); + + it('returns true for Opera >= 40', () => { + const userAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991'; + expect(canInjectU2fApi(userAgent)).toBe(true); + }); + + it('returns false for Safari', () => { + const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4'; + expect(canInjectU2fApi(userAgent)).toBe(false); + }); + + it('returns false for Chrome on Android', () => { + const userAgent = 'Mozilla/5.0 (Linux; Android 7.0; VS988 Build/NRD90U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3145.0 Mobile Safari/537.36'; + expect(canInjectU2fApi(userAgent)).toBe(false); + }); + + it('returns false for Chrome on iOS', () => { + const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1'; + expect(canInjectU2fApi(userAgent)).toBe(false); + }); + + it('returns false for Safari on iOS', () => { + const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1'; + expect(canInjectU2fApi(userAgent)).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js new file mode 100644 index 00000000000..67056793a20 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; + +import LabelsSelect from '~/labels_select'; +import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (config = mockConfig) => { + const Component = Vue.extend(baseComponent); + + return mountComponent(Component, config); +}; + +describe('BaseComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('hiddenInputName', () => { + it('returns correct string when showCreate prop is `true`', () => { + expect(vm.hiddenInputName).toBe('issue[label_names][]'); + }); + + it('returns correct string when showCreate prop is `false`', () => { + const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false }); + const vmNonEditable = createComponent(mockConfigNonEditable); + expect(vmNonEditable.hiddenInputName).toBe('label_id[]'); + vmNonEditable.$destroy(); + }); + }); + }); + + describe('methods', () => { + describe('handleClick', () => { + it('emits onLabelClick event with label and list of labels as params', () => { + spyOn(vm, '$emit'); + vm.handleClick(mockLabels[0]); + expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]); + }); + }); + }); + + describe('mounted', () => { + it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => { + expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true); + }); + }); + + describe('template', () => { + it('renders component container element with classes `block labels`', () => { + expect(vm.$el.classList.contains('block')).toBe(true); + expect(vm.$el.classList.contains('labels')).toBe(true); + }); + + it('renders `.selectbox` element', () => { + expect(vm.$el.querySelector('.selectbox')).not.toBeNull(); + expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;'); + }); + + it('renders `.dropdown` element', () => { + expect(vm.$el.querySelector('.dropdown')).not.toBeNull(); + }); + + it('renders `.dropdown-menu` element', () => { + const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu'); + expect(dropdownMenuEl).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull(); + expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js new file mode 100644 index 00000000000..ec63ac306d0 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js @@ -0,0 +1,82 @@ +import Vue from 'vue'; + +import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const componentConfig = Object.assign({}, mockConfig, { + fieldName: 'label_id[]', + labels: mockLabels, + showExtraOptions: false, +}); + +const createComponent = (config = componentConfig) => { + const Component = Vue.extend(dropdownButtonComponent); + + return mountComponent(Component, config); +}; + +describe('DropdownButtonComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('dropdownToggleText', () => { + it('returns text as `Label` when `labels` prop is empty array', () => { + const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] }); + const vmEmptyLabels = createComponent(mockEmptyLabels); + expect(vmEmptyLabels.dropdownToggleText).toBe('Label'); + vmEmptyLabels.$destroy(); + }); + + it('returns first label name with remaining label count when `labels` prop has more than one item', () => { + const mockMoreLabels = Object.assign({}, componentConfig, { + labels: mockLabels.concat(mockLabels), + }); + const vmMoreLabels = createComponent(mockMoreLabels); + expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more'); + vmMoreLabels.$destroy(); + }); + + it('returns first label name when `labels` prop has only one item present', () => { + expect(vm.dropdownToggleText).toBe('Foo Label'); + }); + }); + }); + + describe('template', () => { + it('renders component container element of type `button`', () => { + expect(vm.$el.nodeName).toBe('BUTTON'); + }); + + it('renders component container element with required data attributes', () => { + expect(vm.$el.dataset.abilityName).toBe(vm.abilityName); + expect(vm.$el.dataset.fieldName).toBe(vm.fieldName); + expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath); + expect(vm.$el.dataset.labels).toBe(vm.labelsPath); + expect(vm.$el.dataset.namespacePath).toBe(vm.namespace); + expect(vm.$el.dataset.showAny).not.toBeDefined(); + }); + + it('renders dropdown toggle text element', () => { + const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text'); + expect(dropdownToggleTextEl).not.toBeNull(); + expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label'); + }); + + it('renders dropdown button icon', () => { + const dropdownIconEl = vm.$el.querySelector('i.fa'); + expect(dropdownIconEl).not.toBeNull(); + expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js new file mode 100644 index 00000000000..f07aefb2f87 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; + +import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue'; + +import { mockSuggestedColors } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownCreateLabelComponent); + + return mountComponent(Component); +}; + +describe('DropdownCreateLabelComponent', () => { + let vm; + + beforeEach(() => { + gon.suggested_label_colors = mockSuggestedColors; + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('created', () => { + it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => { + expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length); + }); + }); + + describe('template', () => { + it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => { + expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true); + }); + + it('renders `Go back` button on component header', () => { + const backButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-back'); + expect(backButtonEl).not.toBe(null); + expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null); + }); + + it('renders component header element', () => { + const headerEl = vm.$el.querySelector('.dropdown-title'); + expect(headerEl.innerText.trim()).toContain('Create new label'); + }); + + it('renders `Close` button on component header', () => { + const closeButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close'); + expect(closeButtonEl).not.toBe(null); + expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null); + }); + + it('renders `Name new label` input element', () => { + expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null); + expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null); + }); + + it('renders suggested colors list elements', () => { + const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown'); + expect(colorsListContainerEl).not.toBe(null); + expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length); + + const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0]; + expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]); + expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);'); + }); + + it('renders color input element', () => { + expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null); + expect(vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview')).not.toBe(null); + expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null); + }); + + it('renders component action buttons', () => { + const createBtnEl = vm.$el.querySelector('button.js-new-label-btn'); + const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn'); + expect(createBtnEl).not.toBe(null); + expect(createBtnEl.innerText.trim()).toBe('Create'); + expect(cancelBtnEl.innerText.trim()).toBe('Cancel'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js new file mode 100644 index 00000000000..809e0327b1c --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; + +import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue'; + +import { mockConfig } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (labelsWebUrl = mockConfig.labelsWebUrl) => { + const Component = Vue.extend(dropdownFooterComponent); + + return mountComponent(Component, { + labelsWebUrl, + }); +}; + +describe('DropdownFooterComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders `Create new label` link element', () => { + const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page'); + expect(createLabelEl).not.toBeNull(); + expect(createLabelEl.innerText.trim()).toBe('Create new label'); + }); + + it('renders `Manage labels` link element', () => { + const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link'); + expect(manageLabelsEl).not.toBeNull(); + expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl); + expect(manageLabelsEl.innerText.trim()).toBe('Manage labels'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js new file mode 100644 index 00000000000..325fa47c957 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js @@ -0,0 +1,36 @@ +import Vue from 'vue'; + +import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownHeaderComponent); + + return mountComponent(Component); +}; + +describe('DropdownHeaderComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders header text element', () => { + const headerEl = vm.$el.querySelector('.dropdown-title span'); + expect(headerEl.innerText.trim()).toBe('Assign labels'); + }); + + it('renders `Close` button element', () => { + const closeBtnEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close'); + expect(closeBtnEl).not.toBeNull(); + expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js new file mode 100644 index 00000000000..703b87498c7 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; + +import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue'; + +import { mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (name = 'label_id[]', label = mockLabels[0]) => { + const Component = Vue.extend(dropdownHiddenInputComponent); + + return mountComponent(Component, { + name, + label, + }); +}; + +describe('DropdownHiddenInputComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders input element of type `hidden`', () => { + expect(vm.$el.nodeName).toBe('INPUT'); + expect(vm.$el.getAttribute('type')).toBe('hidden'); + expect(vm.$el.getAttribute('name')).toBe(vm.name); + expect(vm.$el.getAttribute('value')).toBe(`${vm.label.id}`); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js new file mode 100644 index 00000000000..69e11d966c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; + +import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(dropdownSearchInputComponent); + + return mountComponent(Component); +}; + +describe('DropdownSearchInputComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders input element with type `search`', () => { + const inputEl = vm.$el.querySelector('input.dropdown-input-field'); + expect(inputEl).not.toBeNull(); + expect(inputEl.getAttribute('type')).toBe('search'); + }); + + it('renders search icon element', () => { + expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull(); + }); + + it('renders clear search icon element', () => { + expect(vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear')).not.toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js new file mode 100644 index 00000000000..c3580933072 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js @@ -0,0 +1,42 @@ +import Vue from 'vue'; + +import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (canEdit = true) => { + const Component = Vue.extend(dropdownTitleComponent); + + return mountComponent(Component, { + canEdit, + }); +}; + +describe('DropdownTitleComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('template', () => { + it('renders title text', () => { + expect(vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true); + expect(vm.$el.innerText.trim()).toContain('Labels'); + }); + + it('renders spinner icon element', () => { + expect(vm.$el.querySelector('.fa-spinner.fa-spin.block-loading')).not.toBeNull(); + }); + + it('renders `Edit` button element', () => { + const editBtnEl = vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle'); + expect(editBtnEl).not.toBeNull(); + expect(editBtnEl.innerText.trim()).toBe('Edit'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js new file mode 100644 index 00000000000..93b42795bea --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js @@ -0,0 +1,74 @@ +import Vue from 'vue'; + +import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue'; + +import { mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = (labels = mockLabels) => { + const Component = Vue.extend(dropdownValueCollapsedComponent); + + return mountComponent(Component, { + labels, + }); +}; + +describe('DropdownValueCollapsedComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('labelsList', () => { + it('returns empty text when `labels` prop is empty array', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.labelsList).toBe(''); + vmEmptyLabels.$destroy(); + }); + + it('returns labels names separated by coma when `labels` prop has more than one item', () => { + const vmMoreLabels = createComponent(mockLabels.concat(mockLabels)); + expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label'); + vmMoreLabels.$destroy(); + }); + + it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => { + const mockMoreLabels = Object.assign([], mockLabels); + for (let i = 0; i < 6; i += 1) { + mockMoreLabels.unshift(mockLabels[0]); + } + + const vmMoreLabels = createComponent(mockMoreLabels); + expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more'); + vmMoreLabels.$destroy(); + }); + + it('returns first label name when `labels` prop has only one item present', () => { + expect(vm.labelsList).toBe('Foo Label'); + }); + }); + }); + + describe('template', () => { + it('renders component container element with tooltip`', () => { + expect(vm.$el.dataset.placement).toBe('left'); + expect(vm.$el.dataset.container).toBe('body'); + expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList); + }); + + it('renders tags icon element', () => { + expect(vm.$el.querySelector('.fa-tags')).not.toBeNull(); + }); + + it('renders labels count', () => { + expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js new file mode 100644 index 00000000000..66e0957b431 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -0,0 +1,94 @@ +import Vue from 'vue'; + +import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue'; + +import { mockConfig, mockLabels } from './mock_data'; + +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +const createComponent = ( + labels = mockLabels, + labelFilterBasePath = mockConfig.labelFilterBasePath, +) => { + const Component = Vue.extend(dropdownValueComponent); + + return mountComponent(Component, { + labels, + labelFilterBasePath, + }); +}; + +describe('DropdownValueComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('isEmpty', () => { + it('returns true if `labels` prop is empty', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.isEmpty).toBe(true); + vmEmptyLabels.$destroy(); + }); + + it('returns false if `labels` prop is empty', () => { + expect(vm.isEmpty).toBe(false); + }); + }); + }); + + describe('methods', () => { + describe('labelFilterUrl', () => { + it('returns URL string starting with labelFilterBasePath and encoded label.title', () => { + expect(vm.labelFilterUrl({ + title: 'Foo bar', + })).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20bar'); + }); + }); + + describe('labelStyle', () => { + it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => { + const label = { + textColor: '#FFFFFF', + color: '#BADA55', + }; + const styleObj = vm.labelStyle(label); + + expect(styleObj.color).toBe(label.textColor); + expect(styleObj.backgroundColor).toBe(label.color); + }); + }); + }); + + describe('template', () => { + it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => { + expect(vm.$el.classList.contains('hide-collapsed', 'value', 'issuable-show-labels')).toBe(true); + }); + + it('render slot content inside component when `labels` prop is empty', () => { + const vmEmptyLabels = createComponent([]); + expect(vmEmptyLabels.$el.querySelector('.text-secondary').innerText.trim()).toBe(mockConfig.emptyValueText); + vmEmptyLabels.$destroy(); + }); + + it('renders label element with filter URL', () => { + expect(vm.$el.querySelector('a').getAttribute('href')).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20Label'); + }); + + it('renders label element with tooltip and styles based on label details', () => { + const labelEl = vm.$el.querySelector('a span.label.color-label'); + expect(labelEl).not.toBeNull(); + expect(labelEl.dataset.placement).toBe('bottom'); + expect(labelEl.dataset.container).toBe('body'); + expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description); + expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);'); + expect(labelEl.innerText.trim()).toBe(mockLabels[0].title); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js new file mode 100644 index 00000000000..e9008c29b22 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js @@ -0,0 +1,49 @@ +export const mockLabels = [ + { + id: 26, + title: 'Foo Label', + description: 'Foobar', + color: '#BADA55', + text_color: '#FFFFFF', + }, +]; + +export const mockSuggestedColors = [ + '#0033CC', + '#428BCA', + '#44AD8E', + '#A8D695', + '#5CB85C', + '#69D100', + '#004E00', + '#34495E', + '#7F8C8D', + '#A295D6', + '#5843AD', + '#8E44AD', + '#FFECDB', + '#AD4363', + '#D10069', + '#CC0033', + '#FF0000', + '#D9534F', + '#D1D100', + '#F0AD4E', + '#AD8D43', +]; + +export const mockConfig = { + showCreate: true, + abilityName: 'issue', + context: { + labels: mockLabels, + }, + namespace: 'gitlab-org', + updatePath: '/gitlab-org/my-project/issue/1', + labelsPath: '/gitlab-org/my-project/labels.json', + labelsWebUrl: '/gitlab-org/my-project/labels', + labelFilterBasePath: '/gitlab-org/my-project/issues', + canEdit: true, + suggestedColors: mockSuggestedColors, + emptyValueText: 'None', +}; diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index f7b1a61f4f8..a9b5ed1112a 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -28,6 +28,23 @@ describe Backup::Repository do end describe '#restore' do + subject { described_class.new } + + let(:timestamp) { Time.utc(2017, 3, 22) } + let(:temp_dirs) do + Gitlab.config.repositories.storages.map do |name, storage| + File.join(storage['path'], '..', 'repositories.old.' + timestamp.to_i.to_s) + end + end + + around do |example| + Timecop.freeze(timestamp) { example.run } + end + + after do + temp_dirs.each { |path| FileUtils.rm_rf(path) } + end + describe 'command failure' do before do allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) @@ -35,7 +52,7 @@ describe Backup::Repository do context 'hashed storage' do it 'shows the appropriate error' do - described_class.new.restore + subject.restore expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} (#{project.disk_path}) - error") end @@ -45,7 +62,7 @@ describe Backup::Repository do let!(:project) { create(:project, :legacy_storage) } it 'shows the appropriate error' do - described_class.new.restore + subject.restore expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") end diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index b7c2ff03125..b502daea418 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -4,6 +4,7 @@ describe Banzai::Filter::AutolinkFilter do include FilterSpecHelper let(:link) { 'http://about.gitlab.com/' } + let(:quotes) { ['"', "'"] } it 'does nothing when :autolink is false' do exp = act = link @@ -15,17 +16,7 @@ describe Banzai::Filter::AutolinkFilter do expect(filter(act).to_html).to eq exp end - context 'when the input contains no links' do - it 'does not parse_html back the rinku returned value' do - act = HTML::Pipeline.parse('<p>This text contains no links to autolink</p>') - - expect_any_instance_of(described_class).not_to receive(:parse_html) - - filter(act).to_html - end - end - - context 'Rinku schemes' do + context 'Various schemes' do it 'autolinks http' do doc = filter("See #{link}") expect(doc.at_css('a').text).to eq link @@ -56,32 +47,26 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a')['href']).to eq link end - it 'accepts link_attr options' do - doc = filter("See #{link}", link_attr: { class: 'custom' }) + it 'autolinks multiple URLs' do + link1 = 'http://localhost:3000/' + link2 = 'http://google.com/' - expect(doc.at_css('a')['class']).to eq 'custom' - end + doc = filter("See #{link1} and #{link2}") - described_class::IGNORE_PARENTS.each do |elem| - it "ignores valid links contained inside '#{elem}' element" do - exp = act = "<#{elem}>See #{link}</#{elem}>" - expect(filter(act).to_html).to eq exp - end - end + found_links = doc.css('a') - context 'when the input contains link' do - it 'does parse_html back the rinku returned value' do - act = HTML::Pipeline.parse("<p>See #{link}</p>") + expect(found_links.size).to eq(2) + expect(found_links[0].text).to eq(link1) + expect(found_links[0]['href']).to eq(link1) + expect(found_links[1].text).to eq(link2) + expect(found_links[1]['href']).to eq(link2) + end - expect_any_instance_of(described_class).to receive(:parse_html).at_least(:once).and_call_original + it 'accepts link_attr options' do + doc = filter("See #{link}", link_attr: { class: 'custom' }) - filter(act).to_html - end + expect(doc.at_css('a')['class']).to eq 'custom' end - end - - context 'other schemes' do - let(:link) { 'foo://bar.baz/' } it 'autolinks smb' do link = 'smb:///Volumes/shared/foo.pdf' @@ -91,6 +76,21 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a')['href']).to eq link end + it 'autolinks multiple occurences of smb' do + link1 = 'smb:///Volumes/shared/foo.pdf' + link2 = 'smb:///Volumes/shared/bar.pdf' + + doc = filter("See #{link1} and #{link2}") + + found_links = doc.css('a') + + expect(found_links.size).to eq(2) + expect(found_links[0].text).to eq(link1) + expect(found_links[0]['href']).to eq(link1) + expect(found_links[1].text).to eq(link2) + expect(found_links[1]['href']).to eq(link2) + end + it 'autolinks irc' do link = 'irc://irc.freenode.net/git' doc = filter("See #{link}") @@ -132,6 +132,45 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a').text).to eq link end + it 'includes trailing punctuation when part of a balanced pair' do + described_class::PUNCTUATION_PAIRS.each do |close, open| + next if open.in?(quotes) + + balanced_link = "#{link}#{open}abc#{close}" + balanced_actual = filter("See #{balanced_link}...") + unbalanced_link = "#{link}#{close}" + unbalanced_actual = filter("See #{unbalanced_link}...") + + expect(balanced_actual.at_css('a').text).to eq(balanced_link) + expect(unescape(balanced_actual.to_html)).to eq(Rinku.auto_link("See #{balanced_link}...")) + expect(unbalanced_actual.at_css('a').text).to eq(link) + expect(unescape(unbalanced_actual.to_html)).to eq(Rinku.auto_link("See #{unbalanced_link}...")) + end + end + + it 'removes trailing quotes' do + quotes.each do |quote| + balanced_link = "#{link}#{quote}abc#{quote}" + balanced_actual = filter("See #{balanced_link}...") + unbalanced_link = "#{link}#{quote}" + unbalanced_actual = filter("See #{unbalanced_link}...") + + expect(balanced_actual.at_css('a').text).to eq(balanced_link[0...-1]) + expect(unescape(balanced_actual.to_html)).to eq(Rinku.auto_link("See #{balanced_link}...")) + expect(unbalanced_actual.at_css('a').text).to eq(link) + expect(unescape(unbalanced_actual.to_html)).to eq(Rinku.auto_link("See #{unbalanced_link}...")) + end + end + + it 'removes one closing punctuation mark when the punctuation in the link is unbalanced' do + complicated_link = "(#{link}(a'b[c'd]))'" + expected_complicated_link = %Q{(<a href="#{link}(a'b[c'd]))">#{link}(a'b[c'd]))</a>'} + actual = unescape(filter(complicated_link).to_html) + + expect(actual).to eq(Rinku.auto_link(complicated_link)) + expect(actual).to eq(expected_complicated_link) + end + it 'does not include trailing HTML entities' do doc = filter("See <<<#{link}>>>") @@ -151,4 +190,29 @@ describe Banzai::Filter::AutolinkFilter do end end end + + context 'when the link is inside a tag' do + %w[http rdar].each do |protocol| + it "renders text after the link correctly for #{protocol}" do + doc = filter(ERB::Util.html_escape_once("<#{protocol}://link><another>")) + + expect(doc.children.last.text).to include('<another>') + end + end + end + + # Rinku does not escape these characters in HTML attributes, but content_tag + # does. We don't care about that difference for these specs, though. + def unescape(html) + %w([ ] { }).each do |cgi_escape| + html.sub!(CGI.escape(cgi_escape), cgi_escape) + end + + quotes.each do |html_escape| + html.sub!(CGI.escape_html(html_escape), html_escape) + html.sub!(CGI.escape(html_escape), CGI.escape_html(html_escape)) + end + + html + end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 862b1fe3fd3..0c524a1551f 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -381,11 +381,11 @@ describe Banzai::Filter::LabelReferenceFilter do end it 'has valid link text' do - expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name_with_namespace}" + expect(result.css('a').first.text).to eq "#{label.name} in #{project2.full_name}" end it 'has valid text' do - expect(result.text).to eq "See #{label.name} in #{project2.name_with_namespace}" + expect(result.text).to eq "See #{label.name} in #{project2.full_name}" end it 'ignores invalid IDs on the referenced label' do @@ -481,12 +481,12 @@ describe Banzai::Filter::LabelReferenceFilter do it 'has valid link text' do expect(result.css('a').first.text) - .to eq "#{group_label.name} in #{another_project.name_with_namespace}" + .to eq "#{group_label.name} in #{another_project.full_name}" end it 'has valid text' do expect(result.text) - .to eq "See #{group_label.name} in #{another_project.name_with_namespace}" + .to eq "See #{group_label.name} in #{another_project.full_name}" end it 'ignores invalid IDs on the referenced label' do diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb index 6a47350be81..9b3916bf9e3 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/auth/ldap/access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::LDAP::Access do +describe Gitlab::Auth::LDAP::Access do let(:access) { described_class.new user } let(:user) { create(:omniauth_user) } @@ -19,7 +19,7 @@ describe Gitlab::LDAP::Access do context 'when the user cannot be found' do before do - allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil) end it { is_expected.to be_falsey } @@ -33,12 +33,12 @@ describe Gitlab::LDAP::Access do context 'when the user is found' do before do - allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user) end context 'and the user is disabled via active directory' do before do - allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true) + allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true) end it { is_expected.to be_falsey } @@ -52,7 +52,7 @@ describe Gitlab::LDAP::Access do context 'and has no disabled flag in active diretory' do before do - allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false) + allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false) end it { is_expected.to be_truthy } @@ -87,15 +87,15 @@ describe Gitlab::LDAP::Access do context 'without ActiveDirectory enabled' do before do - allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) - allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false) + allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true) + allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive(:active_directory).and_return(false) end it { is_expected.to be_truthy } context 'when user cannot be found' do before do - allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil) end it { is_expected.to be_falsey } diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/auth/ldap/adapter_spec.rb index 6132abd9b35..10c60d792bd 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/auth/ldap/adapter_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::LDAP::Adapter do +describe Gitlab::Auth::LDAP::Adapter do include LdapHelpers let(:ldap) { double(:ldap) } @@ -139,6 +139,6 @@ describe Gitlab::LDAP::Adapter do end def ldap_attributes - Gitlab::LDAP::Person.ldap_attributes(Gitlab::LDAP::Config.new('ldapmain')) + Gitlab::Auth::LDAP::Person.ldap_attributes(Gitlab::Auth::LDAP::Config.new('ldapmain')) end end diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb index 9c30ddd7fe2..05541972f87 100644 --- a/spec/lib/gitlab/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::LDAP::AuthHash do +describe Gitlab::Auth::LDAP::AuthHash do include LdapHelpers let(:auth_hash) do @@ -56,7 +56,7 @@ describe Gitlab::LDAP::AuthHash do end before do - allow_any_instance_of(Gitlab::LDAP::Config).to receive(:attributes).and_return(attributes) + allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive(:attributes).and_return(attributes) end it "has the correct username" do diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/auth/ldap/authentication_spec.rb index 9d57a46c12b..111572d043b 100644 --- a/spec/lib/gitlab/ldap/authentication_spec.rb +++ b/spec/lib/gitlab/auth/ldap/authentication_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' -describe Gitlab::LDAP::Authentication do +describe Gitlab::Auth::LDAP::Authentication do let(:dn) { 'uid=John Smith, ou=People, dc=example, dc=com' } - let(:user) { create(:omniauth_user, extern_uid: Gitlab::LDAP::Person.normalize_dn(dn)) } + let(:user) { create(:omniauth_user, extern_uid: Gitlab::Auth::LDAP::Person.normalize_dn(dn)) } let(:login) { 'john' } let(:password) { 'password' } describe 'login' do before do - allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) + allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true) end it "finds the user if authentication is successful" do @@ -43,7 +43,7 @@ describe Gitlab::LDAP::Authentication do end it "fails if ldap is disabled" do - allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(false) + allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(false) expect(described_class.login(login, password)).to be_falsey end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb index e10837578a8..82587e2ba55 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/auth/ldap/config_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::LDAP::Config do +describe Gitlab::Auth::LDAP::Config do include LdapHelpers let(:config) { described_class.new('ldapmain') } diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/auth/ldap/dn_spec.rb index 8e21ecdf9ab..f2983a02602 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/auth/ldap/dn_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::LDAP::DN do +describe Gitlab::Auth::LDAP::DN do using RSpec::Parameterized::TableSyntax describe '#normalize_value' do @@ -13,7 +13,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'John Smith,' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end @@ -21,7 +21,7 @@ describe Gitlab::LDAP::DN do let(:given) { '#aa aa' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"") + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"") end end @@ -29,7 +29,7 @@ describe Gitlab::LDAP::DN do let(:given) { '#aaXaaa' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"") + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"") end end @@ -37,7 +37,7 @@ describe Gitlab::LDAP::DN do let(:given) { '#aaaYaa' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"") + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"") end end @@ -45,7 +45,7 @@ describe Gitlab::LDAP::DN do let(:given) { '"Sebasti\\cX\\a1n"' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") end end @@ -53,7 +53,7 @@ describe Gitlab::LDAP::DN do let(:given) { '"James' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end @@ -61,7 +61,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'J\ames' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"') end end @@ -69,7 +69,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'foo\\' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end end @@ -86,7 +86,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' } it 'raises UnsupportedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError) end end @@ -95,7 +95,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' } it 'raises UnsupportedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError) end end @@ -103,7 +103,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' } it 'raises UnsupportedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError) end end end @@ -115,7 +115,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid=John Smith,' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end @@ -123,7 +123,7 @@ describe Gitlab::LDAP::DN do let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"") + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"") end end @@ -131,7 +131,7 @@ describe Gitlab::LDAP::DN do let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"") + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"") end end @@ -139,7 +139,7 @@ describe Gitlab::LDAP::DN do let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"") + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"") end end @@ -147,7 +147,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid="Sebasti\\cX\\a1n"' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") end end @@ -155,7 +155,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'John' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end @@ -163,7 +163,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'cn="James' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end @@ -171,7 +171,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'cn=J\ames' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"') end end @@ -179,7 +179,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'cn=\\' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end @@ -187,7 +187,7 @@ describe Gitlab::LDAP::DN do let(:given) { '1.2.d=Value' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"') end end @@ -195,7 +195,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'd1.2=Value' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."') end end @@ -203,7 +203,7 @@ describe Gitlab::LDAP::DN do let(:given) { ' -uid=John Smith' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"') end end @@ -211,7 +211,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid\\=john' } it 'raises MalformedError' do - expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"') + expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"') end end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/auth/ldap/person_spec.rb index 05e1e394bb1..1527fe60fb9 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/auth/ldap/person_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::LDAP::Person do +describe Gitlab::Auth::LDAP::Person do include LdapHelpers let(:entry) { ldap_user_entry('john.doe') } @@ -59,7 +59,7 @@ describe Gitlab::LDAP::Person do } } ) - config = Gitlab::LDAP::Config.new('ldapmain') + config = Gitlab::Auth::LDAP::Config.new('ldapmain') ldap_attributes = described_class.ldap_attributes(config) expect(ldap_attributes).to match_array(%w(dn uid cn mail memberof)) diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb index 048caa38fcf..cab2169593a 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/auth/ldap/user_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::LDAP::User do +describe Gitlab::Auth::LDAP::User do let(:ldap_user) { described_class.new(auth_hash) } let(:gl_user) { ldap_user.gl_user } let(:info) do @@ -177,7 +177,7 @@ describe Gitlab::LDAP::User do describe 'blocking' do def configure_block(value) - allow_any_instance_of(Gitlab::LDAP::Config) + allow_any_instance_of(Gitlab::Auth::LDAP::Config) .to receive(:block_auto_created_users).and_return(value) end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb index dbcc200b90b..40001cea22e 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::OAuth::AuthHash do +describe Gitlab::Auth::OAuth::AuthHash do let(:provider) { 'ldap'.freeze } let(:auth_hash) do described_class.new( diff --git a/spec/lib/gitlab/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb index 30faf107e3f..fc35d430917 100644 --- a/spec/lib/gitlab/o_auth/provider_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::OAuth::Provider do +describe Gitlab::Auth::OAuth::Provider do describe '#config_for' do context 'for an LDAP provider' do context 'when the provider exists' do diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index b8455403bdb..0c71f1d8ca6 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::OAuth::User do +describe Gitlab::Auth::OAuth::User do let(:oauth_user) { described_class.new(auth_hash) } let(:gl_user) { oauth_user.gl_user } let(:uid) { 'my-uid' } @@ -18,7 +18,7 @@ describe Gitlab::OAuth::User do } } end - let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } + let(:ldap_user) { Gitlab::Auth::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } describe '#persisted?' do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } @@ -39,7 +39,7 @@ describe Gitlab::OAuth::User do describe '#save' do def stub_ldap_config(messages) - allow(Gitlab::LDAP::Config).to receive_messages(messages) + allow(Gitlab::Auth::LDAP::Config).to receive_messages(messages) end let(:provider) { 'twitter' } @@ -215,7 +215,7 @@ describe Gitlab::OAuth::User do context "and no account for the LDAP user" do before do - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) oauth_user.save end @@ -250,7 +250,7 @@ describe Gitlab::OAuth::User do context "and LDAP user has an account already" do let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } it "adds the omniauth identity to the LDAP account" do - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) oauth_user.save @@ -270,8 +270,8 @@ describe Gitlab::OAuth::User do context 'when an LDAP person is not found by uid' do it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) - allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user) oauth_user.save @@ -297,7 +297,7 @@ describe Gitlab::OAuth::User do context 'and no account for the LDAP user' do it 'creates a user favoring the LDAP username and strips email domain' do - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) oauth_user.save @@ -309,7 +309,7 @@ describe Gitlab::OAuth::User do context "and no corresponding LDAP person" do before do - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(nil) end include_examples "to verify compliance with allow_single_sign_on" @@ -358,13 +358,13 @@ describe Gitlab::OAuth::User do allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } allow(ldap_user).to receive(:dn) { dn } - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) end context "and no account for the LDAP user" do context 'dont block on create (LDAP)' do before do - allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false) end it do @@ -376,7 +376,7 @@ describe Gitlab::OAuth::User do context 'block on create (LDAP)' do before do - allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true) end it do @@ -392,7 +392,7 @@ describe Gitlab::OAuth::User do context 'dont block on create (LDAP)' do before do - allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false) end it do @@ -404,7 +404,7 @@ describe Gitlab::OAuth::User do context 'block on create (LDAP)' do before do - allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true) end it do @@ -448,7 +448,7 @@ describe Gitlab::OAuth::User do context 'dont block on create (LDAP)' do before do - allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) + allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false) end it do @@ -460,7 +460,7 @@ describe Gitlab::OAuth::User do context 'block on create (LDAP)' do before do - allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) + allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true) end it do diff --git a/spec/lib/gitlab/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb index a555935aea3..bb950e6bbf8 100644 --- a/spec/lib/gitlab/saml/auth_hash_spec.rb +++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Saml::AuthHash do +describe Gitlab::Auth::Saml::AuthHash do include LoginHelpers let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers) } } diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb index 1765980e977..62514ca0688 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/auth/saml/user_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Saml::User do +describe Gitlab::Auth::Saml::User do include LdapHelpers include LoginHelpers @@ -17,7 +17,7 @@ describe Gitlab::Saml::User do email: 'john@mail.com' } end - let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } + let(:ldap_user) { Gitlab::Auth::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } describe '#save' do before do @@ -159,10 +159,10 @@ describe Gitlab::Saml::User do allow(ldap_user).to receive(:username) { uid } allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) } allow(ldap_user).to receive(:dn) { dn } - allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter) - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user) - allow(Gitlab::LDAP::Person).to receive(:find_by_dn).with(dn, adapter).and_return(ldap_user) - allow(Gitlab::LDAP::Person).to receive(:find_by_email).with('john@mail.com', adapter).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Adapter).to receive(:new).and_return(adapter) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).with(dn, adapter).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).with('john@mail.com', adapter).and_return(ldap_user) end context 'and no account for the LDAP user' do @@ -210,10 +210,10 @@ describe Gitlab::Saml::User do nil_types = uid_types - [uid_type] nil_types.each do |type| - allow(Gitlab::LDAP::Person).to receive(:"find_by_#{type}").and_return(nil) + allow(Gitlab::Auth::LDAP::Person).to receive(:"find_by_#{type}").and_return(nil) end - allow(Gitlab::LDAP::Person).to receive(:"find_by_#{uid_type}").and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:"find_by_#{uid_type}").and_return(ldap_user) end it 'adds the omniauth identity to the LDAP account' do @@ -280,7 +280,7 @@ describe Gitlab::Saml::User do it 'adds the LDAP identity to the existing SAML user' do create(:omniauth_user, email: 'john@mail.com', extern_uid: dn, provider: 'saml', username: 'john') - allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(dn, adapter).and_return(ldap_user) + allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).with(dn, adapter).and_return(ldap_user) local_hash = OmniAuth::AuthHash.new(uid: dn, provider: provider, info: info_hash) local_saml_user = described_class.new(local_hash) diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index cc202ce8bca..f969f9e8e38 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -309,17 +309,17 @@ describe Gitlab::Auth do context "with ldap enabled" do before do - allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) + allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true) end it "tries to autheticate with db before ldap" do - expect(Gitlab::LDAP::Authentication).not_to receive(:login) + expect(Gitlab::Auth::LDAP::Authentication).not_to receive(:login) gl_auth.find_with_user_password(username, password) end it "uses ldap as fallback to for authentication" do - expect(Gitlab::LDAP::Authentication).to receive(:login) + expect(Gitlab::Auth::LDAP::Authentication).to receive(:login) gl_auth.find_with_user_password('ldap_user', 'password') end @@ -336,7 +336,7 @@ describe Gitlab::Auth do context "with ldap enabled" do before do - allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) + allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true) end it "does not find non-ldap user by valid login/password" do diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb index 17756621221..7201e4f7bf6 100644 --- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb +++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb @@ -2,23 +2,25 @@ require 'spec_helper' describe Gitlab::Checks::LfsIntegrity do include ProjectForksHelper + let(:project) { create(:project, :repository) } - let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } + let(:repository) { project.repository } + let(:newrev) do + operations = BareRepoOperations.new(repository.path) + + # Create a commit not pointed at by any ref to emulate being in the + # pre-receive hook so that `--not --all` returns some objects + operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects") + end subject { described_class.new(project, newrev) } describe '#objects_missing?' do - let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } - - before do - allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block| - lazy_block.call([blob_object.id]) - end - end + let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') } context 'with LFS not enabled' do it 'skips integrity check' do - expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects) + expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers) subject.objects_missing? end @@ -33,7 +35,7 @@ describe Gitlab::Checks::LfsIntegrity do let(:newrev) { nil } it 'skips integrity check' do - expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects) + expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers) expect(subject.objects_missing?).to be_falsey end diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index 49a179ba875..167876ca158 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::ContributionsCalendar do end let(:public_project) do - create(:project, :public) do |project| + create(:project, :public, :repository) do |project| create(:project_member, user: contributor, project: project) end end @@ -40,13 +40,13 @@ describe Gitlab::ContributionsCalendar do described_class.new(contributor, current_user) end - def create_event(project, day, hour = 0) + def create_event(project, day, hour = 0, action = Event::CREATED, target_symbol = :issue) @targets ||= {} - @targets[project] ||= create(:issue, project: project, author: contributor) + @targets[project] ||= create(target_symbol, project: project, author: contributor) Event.create!( project: project, - action: Event::CREATED, + action: action, target: @targets[project], author: contributor, created_at: DateTime.new(day.year, day.month, day.day, hour) @@ -71,6 +71,12 @@ describe Gitlab::ContributionsCalendar do expect(calendar(contributor).activity_dates[today]).to eq(2) end + it "counts the diff notes on merge request" do + create_event(public_project, today, 0, Event::COMMENTED, :diff_note_on_merge_request) + + expect(calendar(contributor).activity_dates[today]).to eq(1) + end + context "when events fall under different dates depending on the time zone" do before do create_event(public_project, today, 1) diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb index 3fe0493ed9b..8b07da11c5d 100644 --- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -41,7 +41,7 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do milestone = create(:milestone, project: project) issue.update(milestone: milestone) - create_merge_request_closing_issue(issue) + create_merge_request_closing_issue(user, project, issue) end end end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 38a47a159e1..397dd4e5d2c 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -236,8 +236,8 @@ describe 'cycle analytics events' do pipeline.run! pipeline.succeed! - merge_merge_requests_closing_issue(context) - deploy_master + merge_merge_requests_closing_issue(user, project, context) + deploy_master(user, project) end it 'has the name' do @@ -294,8 +294,8 @@ describe 'cycle analytics events' do let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } before do - merge_merge_requests_closing_issue(context) - deploy_master + merge_merge_requests_closing_issue(user, project, context) + deploy_master(user, project) end it 'has the total time' do @@ -334,7 +334,7 @@ describe 'cycle analytics events' do def setup(context) milestone = create(:milestone, project: project) context.update(milestone: milestone) - mr = create_merge_request_closing_issue(context, commit_message: "References #{context.to_reference}") + mr = create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}") ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) end diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb new file mode 100644 index 00000000000..56a316318cb --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +describe Gitlab::CycleAnalytics::UsageData do + describe '#to_json' do + before do + Timecop.freeze do + user = create(:user, :admin) + projects = create_list(:project, 2, :repository) + + projects.each_with_index do |project, time| + issue = create(:issue, project: project, created_at: (time + 1).hour.ago) + + allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) + + milestone = create(:milestone, project: project) + mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") + pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) + + create_cycle(user, project, issue, mr, milestone, pipeline) + deploy_master(user, project, environment: 'staging') + deploy_master(user, project) + end + end + end + + shared_examples 'a valid usage data result' do + it 'returns the aggregated usage data of every selected project' do + result = subject.to_json + + expect(result).to have_key(:avg_cycle_analytics) + + CycleAnalytics::STAGES.each do |stage| + expect(result[:avg_cycle_analytics]).to have_key(stage) + + stage_values = result[:avg_cycle_analytics][stage] + expected_values = expect_values_per_stage[stage] + + expected_values.each_pair do |op, value| + expect(stage_values).to have_key(op) + + if op == :missing + expect(stage_values[op]).to eq(value) + else + # delta is used because of git timings that Timecop does not stub + expect(stage_values[op].to_i).to be_within(5).of(value.to_i) + end + end + end + end + end + + context 'when using postgresql', :postgresql do + let(:expect_values_per_stage) do + { + issue: { + average: 5400, + sd: 2545, + missing: 0 + }, + plan: { + average: 2, + sd: 2, + missing: 0 + }, + code: { + average: nil, + sd: 0, + missing: 2 + }, + test: { + average: nil, + sd: 0, + missing: 2 + }, + review: { + average: 0, + sd: 0, + missing: 0 + }, + staging: { + average: 0, + sd: 0, + missing: 0 + }, + production: { + average: 5400, + sd: 2545, + missing: 0 + } + } + end + + it_behaves_like 'a valid usage data result' + end + + context 'when using mysql', :mysql do + let(:expect_values_per_stage) do + { + issue: { + average: nil, + sd: 0, + missing: 2 + }, + plan: { + average: nil, + sd: 0, + missing: 2 + }, + code: { + average: nil, + sd: 0, + missing: 2 + }, + test: { + average: nil, + sd: 0, + missing: 2 + }, + review: { + average: nil, + sd: 0, + missing: 2 + }, + staging: { + average: nil, + sd: 0, + missing: 2 + }, + production: { + average: nil, + sd: 0, + missing: 2 + } + } + end + + it_behaves_like 'a valid usage data result' + end + end +end diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index 91c43f2bdc0..ee91decafad 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::DataBuilder::Build do it { expect(data[:build_status]).to eq(build.status) } it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } - it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } + it { expect(data[:project_name]).to eq(build.project.full_name) } context 'commit author_url' do context 'when no commit present' do diff --git a/spec/lib/gitlab/database/median_spec.rb b/spec/lib/gitlab/database/median_spec.rb new file mode 100644 index 00000000000..1b5e30089ce --- /dev/null +++ b/spec/lib/gitlab/database/median_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::Database::Median do + let(:dummy_class) do + Class.new do + include Gitlab::Database::Median + end + end + + subject(:median) { dummy_class.new } + + describe '#median_datetimes' do + it 'raises NotSupportedError', :mysql do + expect { median.median_datetimes(nil, nil, nil, :project_id) }.to raise_error(dummy_class::NotSupportedError, "partition_column is not supported for MySQL") + end + end +end diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 031efcf1291..53899e00b53 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -55,8 +55,8 @@ describe Gitlab::Email::Handler::CreateNoteHandler do expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) end - context 'because the note was commands only' do - let!(:email_raw) { fixture_file("emails/commands_only_reply.eml") } + context 'because the note was update commands only' do + let!(:email_raw) { fixture_file("emails/update_commands_only_reply.eml") } context 'and current user cannot update noteable' do it 'raises a CommandsOnlyNoteError' do @@ -70,13 +70,10 @@ describe Gitlab::Email::Handler::CreateNoteHandler do end it 'does not raise an error' do - expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy - # One system note is created for the 'close' event expect { receiver.execute }.to change { noteable.notes.count }.by(1) expect(noteable.reload).to be_closed - expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy end end end @@ -85,15 +82,13 @@ describe Gitlab::Email::Handler::CreateNoteHandler do context 'when the note contains quick actions' do let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") } - context 'and current user cannot update noteable' do - it 'post a note and does not update the noteable' do - expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy - - # One system note is created for the new note - expect { receiver.execute }.to change { noteable.notes.count }.by(1) + context 'and current user cannot update the noteable' do + it 'only executes the commands that the user can perform' do + expect { receiver.execute } + .to change { noteable.notes.user.count }.by(1) + .and change { user.todos_pending_count }.from(0).to(1) expect(noteable.reload).to be_open - expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy end end @@ -102,14 +97,14 @@ describe Gitlab::Email::Handler::CreateNoteHandler do project.add_developer(user) end - it 'post a note and updates the noteable' do + it 'posts a note and updates the noteable' do expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy - # One system note is created for the new note, one for the 'close' event - expect { receiver.execute }.to change { noteable.notes.count }.by(2) + expect { receiver.execute } + .to change { noteable.notes.user.count }.by(1) + .and change { user.todos_pending_count }.from(0).to(1) expect(noteable.reload).to be_closed - expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy end end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index a6341cd509b..67d898e787e 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -500,4 +500,33 @@ describe Gitlab::Git::Blob, seed_helper: true do end end end + + describe '#load_all_data!' do + let(:full_data) { 'abcd' } + let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: 'abc') } + + subject { blob.load_all_data!(repository) } + + it 'loads missing data' do + expect(Gitlab::GitalyClient).to receive(:migrate) + .with(:git_blob_load_all_data).and_return(full_data) + + subject + + expect(blob.data).to eq(full_data) + end + + context 'with a fully loaded blob' do + let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: full_data) } + + it "doesn't perform any loading" do + expect(Gitlab::GitalyClient).not_to receive(:migrate) + .with(:git_blob_load_all_data) + + subject + + expect(blob.data).to eq(full_data) + end + end + end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 0b20a6349a2..a05feaac1ca 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -393,81 +393,111 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '.extract_signature' do - subject { described_class.extract_signature(repository, commit_id) } - - shared_examples '.extract_signature' do - context 'when the commit is signed' do - let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - - it 'returns signature and signed text' do - signature, signed_text = subject - - expected_signature = <<~SIGNATURE - -----BEGIN PGP SIGNATURE----- - Version: GnuPG/MacGPG2 v2.0.22 (Darwin) - Comment: GPGTools - https://gpgtools.org + shared_examples 'extracting commit signature' do + context 'when the commit is signed' do + let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + + it 'returns signature and signed text' do + signature, signed_text = subject + + expected_signature = <<~SIGNATURE + -----BEGIN PGP SIGNATURE----- + Version: GnuPG/MacGPG2 v2.0.22 (Darwin) + Comment: GPGTools - https://gpgtools.org + + iQEcBAABCgAGBQJTDvaZAAoJEGJ8X1ifRn8XfvYIAMuB0yrbTGo1BnOSoDfyrjb0 + Kw2EyUzvXYL72B63HMdJ+/0tlSDC6zONF3fc+bBD8z+WjQMTbwFNMRbSSy2rKEh+ + mdRybOP3xBIMGgEph0/kmWln39nmFQBsPRbZBWoU10VfI/ieJdEOgOphszgryRar + TyS73dLBGE9y9NIININVaNISet9D9QeXFqc761CGjh4YIghvPpi+YihMWapGka6v + hgKhX+hc5rj+7IEE0CXmlbYR8OYvAbAArc5vJD7UTxAY4Z7/l9d6Ydt9GQ25khfy + ANFgltYzlR6evLFmDjssiP/mx/ZMN91AL0ueJ9nNGv411Mu2CUW+tDCaQf35mdc= + =j51i + -----END PGP SIGNATURE----- + SIGNATURE + + expect(signature).to eq(expected_signature.chomp) + expect(signature).to be_a_binary_string + + expected_signed_text = <<~SIGNED_TEXT + tree 22bfa2fbd217df24731f43ff43a4a0f8db759dae + parent ae73cb07c9eeaf35924a10f713b364d32b2dd34f + author Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + committer Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + + Feature added + + Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> + SIGNED_TEXT + + expect(signed_text).to eq(expected_signed_text) + expect(signed_text).to be_a_binary_string + end + end - iQEcBAABCgAGBQJTDvaZAAoJEGJ8X1ifRn8XfvYIAMuB0yrbTGo1BnOSoDfyrjb0 - Kw2EyUzvXYL72B63HMdJ+/0tlSDC6zONF3fc+bBD8z+WjQMTbwFNMRbSSy2rKEh+ - mdRybOP3xBIMGgEph0/kmWln39nmFQBsPRbZBWoU10VfI/ieJdEOgOphszgryRar - TyS73dLBGE9y9NIININVaNISet9D9QeXFqc761CGjh4YIghvPpi+YihMWapGka6v - hgKhX+hc5rj+7IEE0CXmlbYR8OYvAbAArc5vJD7UTxAY4Z7/l9d6Ydt9GQ25khfy - ANFgltYzlR6evLFmDjssiP/mx/ZMN91AL0ueJ9nNGv411Mu2CUW+tDCaQf35mdc= - =j51i - -----END PGP SIGNATURE----- - SIGNATURE + context 'when the commit has no signature' do + let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } - expect(signature).to eq(expected_signature.chomp) - expect(signature).to be_a_binary_string + it 'returns nil' do + expect(subject).to be_nil + end + end - expected_signed_text = <<~SIGNED_TEXT - tree 22bfa2fbd217df24731f43ff43a4a0f8db759dae - parent ae73cb07c9eeaf35924a10f713b364d32b2dd34f - author Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 - committer Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + context 'when the commit cannot be found' do + let(:commit_id) { Gitlab::Git::BLANK_SHA } - Feature added + it 'returns nil' do + expect(subject).to be_nil + end + end - Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> - SIGNED_TEXT + context 'when the commit ID is invalid' do + let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e' } - expect(signed_text).to eq(expected_signed_text) - expect(signed_text).to be_a_binary_string - end + it 'raises ArgumentError' do + expect { subject }.to raise_error(ArgumentError) end + end + end - context 'when the commit has no signature' do - let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } - - it 'returns nil' do - expect(subject).to be_nil + describe '.extract_signature_lazily' do + shared_examples 'loading signatures in batch once' do + it 'fetches signatures in batch once' do + commit_ids = %w[0b4bc9a49b562e85de7cc9e834518ea6828729b9 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6] + signatures = commit_ids.map do |commit_id| + described_class.extract_signature_lazily(repository, commit_id) end - end - context 'when the commit cannot be found' do - let(:commit_id) { Gitlab::Git::BLANK_SHA } + expect(described_class).to receive(:batch_signature_extraction) + .with(repository, commit_ids) + .once + .and_return({}) - it 'returns nil' do - expect(subject).to be_nil - end + 2.times { signatures.each(&:itself) } end + end - context 'when the commit ID is invalid' do - let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e' } + subject { described_class.extract_signature_lazily(repository, commit_id).itself } - it 'raises ArgumentError' do - expect { subject }.to raise_error(ArgumentError) - end - end + context 'with Gitaly extract_commit_signature_in_batch feature enabled' do + it_behaves_like 'extracting commit signature' + it_behaves_like 'loading signatures in batch once' + end + + context 'with Gitaly extract_commit_signature_in_batch feature disabled', :disable_gitaly do + it_behaves_like 'extracting commit signature' + it_behaves_like 'loading signatures in batch once' end + end + + describe '.extract_signature' do + subject { described_class.extract_signature(repository, commit_id) } context 'with gitaly' do - it_behaves_like '.extract_signature' + it_behaves_like 'extracting commit signature' end - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '.extract_signature' + context 'without gitaly', :disable_gitaly do + it_behaves_like 'extracting commit signature' end end end diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb index c9007d7d456..d0dd8c6303f 100644 --- a/spec/lib/gitlab/git/lfs_changes_spec.rb +++ b/spec/lib/gitlab/git/lfs_changes_spec.rb @@ -7,34 +7,36 @@ describe Gitlab::Git::LfsChanges do subject { described_class.new(project.repository, newrev) } - describe 'new_pointers' do - before do - allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects).and_yield([blob_object_id]) + describe '#new_pointers' do + shared_examples 'new pointers' do + it 'filters new objects to find lfs pointers' do + expect(subject.new_pointers(not_in: []).first.id).to eq(blob_object_id) + end + + it 'limits new_objects using object_limit' do + expect(subject.new_pointers(object_limit: 1)).to eq([]) + end end - it 'uses rev-list to find new objects' do - rev_list = double - allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) - - expect(rev_list).to receive(:new_objects).and_return([]) - - subject.new_pointers + context 'with gitaly enabled' do + it_behaves_like 'new pointers' end - it 'filters new objects to find lfs pointers' do - expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id]) + context 'with gitaly disabled', :skip_gitaly_mock do + it_behaves_like 'new pointers' - subject.new_pointers(object_limit: 1) - end + it 'uses rev-list to find new objects' do + rev_list = double + allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) - it 'limits new_objects using object_limit' do - expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, []) + expect(rev_list).to receive(:new_objects).and_return([]) - subject.new_pointers(object_limit: 0) + subject.new_pointers + end end end - describe 'all_pointers' do + describe '#all_pointers', :skip_gitaly_mock do it 'uses rev-list to find all objects' do rev_list = double allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list) diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 8e585d9a81c..52c9876cbb6 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -751,255 +751,263 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#log" do - let(:commit_with_old_name) do - Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id) - end - let(:commit_with_new_name) do - Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id) - end - let(:rename_commit) do - Gitlab::Git::Commit.decorate(repository, @rename_commit_id) - end - - before(:context) do - # Add new commits so that there's a renamed file in the commit history - repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged - @commit_with_old_name_id = new_commit_edit_old_file(repo) - @rename_commit_id = new_commit_move_file(repo) - @commit_with_new_name_id = new_commit_edit_new_file(repo) - end - - after(:context) do - # Erase our commits so other tests get the original repo - repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged - repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) - end - - context "where 'follow' == true" do - let(:options) { { ref: "master", follow: true } } + shared_examples 'repository log' do + let(:commit_with_old_name) do + Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id) + end + let(:commit_with_new_name) do + Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id) + end + let(:rename_commit) do + Gitlab::Git::Commit.decorate(repository, @rename_commit_id) + end - context "and 'path' is a directory" do - it "does not follow renames" do - log_commits = repository.log(options.merge(path: "encoding")) + before(:context) do + # Add new commits so that there's a renamed file in the commit history + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + @commit_with_old_name_id = new_commit_edit_old_file(repo) + @rename_commit_id = new_commit_move_file(repo) + @commit_with_new_name_id = new_commit_edit_new_file(repo) + end - aggregate_failures do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_old_name) - end - end + after(:context) do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) end - context "and 'path' is a file that matches the new filename" do - context 'without offset' do - it "follows renames" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG")) + context "where 'follow' == true" do + let(:options) { { ref: "master", follow: true } } + + context "and 'path' is a directory" do + it "does not follow renames" do + log_commits = repository.log(options.merge(path: "encoding")) aggregate_failures do expect(log_commits).to include(commit_with_new_name) expect(log_commits).to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + expect(log_commits).not_to include(commit_with_old_name) end end end - context 'with offset=1' do - it "follows renames and skip the latest commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1)) + context "and 'path' is a file that matches the new filename" do + context 'without offset' do + it "follows renames" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG")) - aggregate_failures do - expect(log_commits).not_to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + aggregate_failures do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end end end - end - context 'with offset=1', 'and limit=1' do - it "follows renames, skip the latest commit and return only one commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1)) + context 'with offset=1' do + it "follows renames and skip the latest commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1)) - expect(log_commits).to contain_exactly(rename_commit) + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end end - end - context 'with offset=1', 'and limit=2' do - it "follows renames, skip the latest commit and return only two commits" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2)) + context 'with offset=1', 'and limit=1' do + it "follows renames, skip the latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1)) - aggregate_failures do - expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name) + expect(log_commits).to contain_exactly(rename_commit) end end - end - context 'with offset=2' do - it "follows renames and skip the latest commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2)) + context 'with offset=1', 'and limit=2' do + it "follows renames, skip the latest commit and return only two commits" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2)) - aggregate_failures do - expect(log_commits).not_to include(commit_with_new_name) - expect(log_commits).not_to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + aggregate_failures do + expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name) + end + end + end + + context 'with offset=2' do + it "follows renames and skip the latest commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end end end - end - context 'with offset=2', 'and limit=1' do - it "follows renames, skip the two latest commit and return only one commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1)) + context 'with offset=2', 'and limit=1' do + it "follows renames, skip the two latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1)) - expect(log_commits).to contain_exactly(commit_with_old_name) + expect(log_commits).to contain_exactly(commit_with_old_name) + end + end + + context 'with offset=2', 'and limit=2' do + it "follows renames, skip the two latest commit and return only one commit" do + log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2)) + + aggregate_failures do + expect(log_commits).not_to include(commit_with_new_name) + expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(commit_with_old_name) + end + end end end - context 'with offset=2', 'and limit=2' do - it "follows renames, skip the two latest commit and return only one commit" do - log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2)) + context "and 'path' is a file that matches the old filename" do + it "does not follow renames" do + log_commits = repository.log(options.merge(path: "CHANGELOG")) aggregate_failures do expect(log_commits).not_to include(commit_with_new_name) - expect(log_commits).not_to include(rename_commit) + expect(log_commits).to include(rename_commit) expect(log_commits).to include(commit_with_old_name) end end end - end - context "and 'path' is a file that matches the old filename" do - it "does not follow renames" do - log_commits = repository.log(options.merge(path: "CHANGELOG")) + context "unknown ref" do + it "returns an empty array" do + log_commits = repository.log(options.merge(ref: 'unknown')) - aggregate_failures do - expect(log_commits).not_to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).to include(commit_with_old_name) + expect(log_commits).to eq([]) end end end - context "unknown ref" do - it "returns an empty array" do - log_commits = repository.log(options.merge(ref: 'unknown')) - - expect(log_commits).to eq([]) - end - end - end + context "where 'follow' == false" do + options = { follow: false } - context "where 'follow' == false" do - options = { follow: false } + context "and 'path' is a directory" do + let(:log_commits) do + repository.log(options.merge(path: "encoding")) + end - context "and 'path' is a directory" do - let(:log_commits) do - repository.log(options.merge(path: "encoding")) + it "does not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end end - it "does not follow renames" do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_old_name) - end - end + context "and 'path' is a file that matches the new filename" do + let(:log_commits) do + repository.log(options.merge(path: "encoding/CHANGELOG")) + end - context "and 'path' is a file that matches the new filename" do - let(:log_commits) do - repository.log(options.merge(path: "encoding/CHANGELOG")) + it "does not follow renames" do + expect(log_commits).to include(commit_with_new_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_old_name) + end end - it "does not follow renames" do - expect(log_commits).to include(commit_with_new_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_old_name) - end - end + context "and 'path' is a file that matches the old filename" do + let(:log_commits) do + repository.log(options.merge(path: "CHANGELOG")) + end - context "and 'path' is a file that matches the old filename" do - let(:log_commits) do - repository.log(options.merge(path: "CHANGELOG")) + it "does not follow renames" do + expect(log_commits).to include(commit_with_old_name) + expect(log_commits).to include(rename_commit) + expect(log_commits).not_to include(commit_with_new_name) + end end - it "does not follow renames" do - expect(log_commits).to include(commit_with_old_name) - expect(log_commits).to include(rename_commit) - expect(log_commits).not_to include(commit_with_new_name) + context "and 'path' includes a directory that used to be a file" do + let(:log_commits) do + repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) + end + + it "returns a list of commits" do + expect(log_commits.size).to eq(1) + end end end - context "and 'path' includes a directory that used to be a file" do - let(:log_commits) do - repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt")) - end + context "where provides 'after' timestamp" do + options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "returns a list of commits" do - expect(log_commits.size).to eq(1) + it "should returns commits on or after that timestamp" do + commits = repository.log(options) + + expect(commits.size).to be > 0 + expect(commits).to satisfy do |commits| + commits.all? { |commit| commit.committed_date >= options[:after] } + end end end - end - context "where provides 'after' timestamp" do - options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') } + context "where provides 'before' timestamp" do + options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "should returns commits on or after that timestamp" do - commits = repository.log(options) + it "should returns commits on or before that timestamp" do + commits = repository.log(options) - expect(commits.size).to be > 0 - expect(commits).to satisfy do |commits| - commits.all? { |commit| commit.committed_date >= options[:after] } + expect(commits.size).to be > 0 + expect(commits).to satisfy do |commits| + commits.all? { |commit| commit.committed_date <= options[:before] } + end end end - end - - context "where provides 'before' timestamp" do - options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') } - it "should returns commits on or before that timestamp" do - commits = repository.log(options) + context 'when multiple paths are provided' do + let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } } - expect(commits.size).to be > 0 - expect(commits).to satisfy do |commits| - commits.all? { |commit| commit.committed_date <= options[:before] } + def commit_files(commit) + commit.rugged_diff_from_parent.deltas.flat_map do |delta| + [delta.old_file[:path], delta.new_file[:path]].uniq.compact + end end - end - end - context 'when multiple paths are provided' do - let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } } + it 'only returns commits matching at least one path' do + commits = repository.log(options) - def commit_files(commit) - commit.rugged_diff_from_parent.deltas.flat_map do |delta| - [delta.old_file[:path], delta.new_file[:path]].uniq.compact + expect(commits.size).to be > 0 + expect(commits).to satisfy do |commits| + commits.none? { |commit| (commit_files(commit) & options[:path]).empty? } + end end end - it 'only returns commits matching at least one path' do - commits = repository.log(options) + context 'limit validation' do + where(:limit) do + [0, nil, '', 'foo'] + end - expect(commits.size).to be > 0 - expect(commits).to satisfy do |commits| - commits.none? { |commit| (commit_files(commit) & options[:path]).empty? } + with_them do + it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) } end end - end - context 'limit validation' do - where(:limit) do - [0, nil, '', 'foo'] - end + context 'with all' do + it 'returns a list of commits' do + commits = repository.log({ all: true, limit: 50 }) - with_them do - it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) } + expect(commits.size).to eq(37) + end end end - context 'with all' do - let(:options) { { all: true, limit: 50 } } - - it 'returns a list of commits' do - commits = repository.log(options) + context 'when Gitaly find_commits feature is enabled' do + it_behaves_like 'repository log' + end - expect(commits.size).to eq(37) - end + context 'when Gitaly find_commits feature is disabled', :disable_gitaly do + it_behaves_like 'repository log' end end @@ -1136,14 +1144,6 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(repository.count_commits(options)).to eq(10) end end - end - - context 'when Gitaly count_commits feature is enabled' do - it_behaves_like 'extended commit counting' - end - - context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do - it_behaves_like 'extended commit counting' context "with all" do it "returns the number of commits in the whole repository" do @@ -1155,10 +1155,18 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'without all or ref being specified' do it "raises an ArgumentError" do - expect { repository.count_commits({}) }.to raise_error(ArgumentError, "Please specify a valid ref or set the 'all' attribute to true") + expect { repository.count_commits({}) }.to raise_error(ArgumentError) end end end + + context 'when Gitaly count_commits feature is enabled' do + it_behaves_like 'extended commit counting' + end + + context 'when Gitaly count_commits feature is disabled', :disable_gitaly do + it_behaves_like 'extended commit counting' + end end describe '#autocrlf' do @@ -1689,6 +1697,35 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#license_short_name' do + shared_examples 'acquiring the Licensee license key' do + subject { repository.license_short_name } + + context 'when no license file can be found' do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw_repository } + + before do + project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master') + end + + it { is_expected.to be_nil } + end + + context 'when an mit license is found' do + it { is_expected.to eq('mit') } + end + end + + context 'when gitaly is enabled' do + it_behaves_like 'acquiring the Licensee license key' + end + + context 'when gitaly is disabled', :disable_gitaly do + it_behaves_like 'acquiring the Licensee license key' + end + end + describe '#with_repo_branch_commit' do context 'when comparing with the same repository' do let(:start_repository) { repository } diff --git a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb new file mode 100644 index 00000000000..a2770ef2fe4 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::BlobService do + let(:project) { create(:project, :repository) } + let(:storage_name) { project.repository_storage } + let(:relative_path) { project.disk_path + '.git' } + let(:repository) { project.repository } + let(:client) { described_class.new(repository) } + + describe '#get_new_lfs_pointers' do + let(:revision) { 'master' } + let(:limit) { 5 } + let(:not_in) { ['branch-a', 'branch-b'] } + let(:expected_params) do + { revision: revision, limit: limit, not_in_refs: not_in, not_in_all: false } + end + + subject { client.get_new_lfs_pointers(revision, limit, not_in) } + + it 'sends a get_new_lfs_pointers message' do + expect_any_instance_of(Gitaly::BlobService::Stub) + .to receive(:get_new_lfs_pointers) + .with(gitaly_request_with_params(expected_params), kind_of(Hash)) + .and_return([]) + + subject + end + + context 'with not_in = :all' do + let(:not_in) { :all } + let(:expected_params) do + { revision: revision, limit: limit, not_in_refs: [], not_in_all: true } + end + + it 'sends the correct message' do + expect_any_instance_of(Gitaly::BlobService::Stub) + .to receive(:get_new_lfs_pointers) + .with(gitaly_request_with_params(expected_params), kind_of(Hash)) + .and_return([]) + + subject + end + end + end + + describe '#get_all_lfs_pointers' do + let(:revision) { 'master' } + + subject { client.get_all_lfs_pointers(revision) } + + it 'sends a get_all_lfs_pointers message' do + expect_any_instance_of(Gitaly::BlobService::Stub) + .to receive(:get_all_lfs_pointers) + .with(gitaly_request_with_params(revision: revision), kind_of(Hash)) + .and_return([]) + + subject + end + end +end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 67c62458f0f..8c6d673391b 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -101,7 +101,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -140,7 +140,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -175,7 +175,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -211,7 +211,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -241,7 +241,7 @@ describe Gitlab::Gpg::Commit do let!(:commit) { create :commit, project: project, sha: commit_sha } before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index c034eccf2a6..6fbffc38444 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do before do allow_any_instance_of(Project).to receive(:commit).and_return(commit) - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return(signature) end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 41a55027f4d..b20cc34dd5c 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -277,6 +277,7 @@ project: - fork_network - custom_attributes - lfs_file_locks +- project_badges award_emoji: - awardable - user @@ -293,3 +294,5 @@ issue_assignees: - assignee lfs_file_locks: - user +project_badges: +- project diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index b6c1f0c81cb..62ef93f847a 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -14,8 +14,7 @@ "template": false, "description": "", "type": "ProjectLabel", - "priorities": [ - ] + "priorities": [] }, { "id": 3, @@ -160,9 +159,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 352, @@ -184,9 +181,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 353, @@ -208,9 +203,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 354, @@ -232,9 +225,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 355, @@ -256,9 +247,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 356, @@ -280,9 +269,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 357, @@ -304,9 +291,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 358, @@ -328,9 +313,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -395,9 +378,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 360, @@ -419,9 +400,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 361, @@ -443,9 +422,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 362, @@ -467,9 +444,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 363, @@ -491,9 +466,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 364, @@ -515,9 +488,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 365, @@ -539,9 +510,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 366, @@ -563,9 +532,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -628,9 +595,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 368, @@ -652,9 +617,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 369, @@ -676,9 +639,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 370, @@ -700,9 +661,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 371, @@ -724,9 +683,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 372, @@ -748,9 +705,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 373, @@ -772,9 +727,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 374, @@ -796,9 +749,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -840,9 +791,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 376, @@ -864,9 +813,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 377, @@ -888,9 +835,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 378, @@ -912,9 +857,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 379, @@ -936,9 +879,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 380, @@ -960,9 +901,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 381, @@ -984,9 +923,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 382, @@ -1008,9 +945,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1052,9 +987,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 384, @@ -1076,9 +1009,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 385, @@ -1100,9 +1031,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 386, @@ -1124,9 +1053,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 387, @@ -1148,9 +1075,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 388, @@ -1172,9 +1097,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 389, @@ -1196,9 +1119,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 390, @@ -1220,9 +1141,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1264,9 +1183,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 392, @@ -1288,9 +1205,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 393, @@ -1312,9 +1227,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 394, @@ -1336,9 +1249,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 395, @@ -1360,9 +1271,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 396, @@ -1384,9 +1293,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 397, @@ -1408,9 +1315,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 398, @@ -1432,9 +1337,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1476,9 +1379,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 400, @@ -1500,9 +1401,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 401, @@ -1524,9 +1423,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 402, @@ -1548,9 +1445,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 403, @@ -1572,9 +1467,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 404, @@ -1596,9 +1489,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 405, @@ -1620,9 +1511,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 406, @@ -1644,9 +1533,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1688,9 +1575,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 408, @@ -1712,9 +1597,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 409, @@ -1736,9 +1619,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 410, @@ -1760,9 +1641,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 411, @@ -1784,9 +1663,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 412, @@ -1808,9 +1685,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 413, @@ -1832,9 +1707,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 414, @@ -1856,9 +1729,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -1900,9 +1771,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 416, @@ -1924,9 +1793,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 417, @@ -1948,9 +1815,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 418, @@ -1972,9 +1837,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 419, @@ -1996,9 +1859,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 420, @@ -2020,9 +1881,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 421, @@ -2044,9 +1903,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 422, @@ -2068,9 +1925,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] }, @@ -2112,9 +1967,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 424, @@ -2136,9 +1989,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 425, @@ -2160,9 +2011,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 426, @@ -2184,9 +2033,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 427, @@ -2208,9 +2055,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 428, @@ -2232,9 +2077,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 429, @@ -2256,9 +2099,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 430, @@ -2280,9 +2121,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ] } @@ -2378,12 +2217,8 @@ ] } ], - "snippets": [ - - ], - "releases": [ - - ], + "snippets": [], + "releases": [], "project_members": [ { "id": 36, @@ -2515,9 +2350,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 672, @@ -2539,9 +2372,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 673, @@ -2563,9 +2394,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 674, @@ -2587,9 +2416,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 675, @@ -2611,9 +2438,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 676, @@ -2635,9 +2460,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 677, @@ -2659,9 +2482,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 678, @@ -2683,9 +2504,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -2696,7 +2515,7 @@ "merge_request_diff_id": 27, "relative_order": 0, "sha": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc", - "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-08-06T08:35:52.000+02:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2708,7 +2527,7 @@ "merge_request_diff_id": 27, "relative_order": 1, "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T10:01:38.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2720,7 +2539,7 @@ "merge_request_diff_id": 27, "relative_order": 2, "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:57:31.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2732,7 +2551,7 @@ "merge_request_diff_id": 27, "relative_order": 3, "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:54:21.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2744,7 +2563,7 @@ "merge_request_diff_id": 27, "relative_order": 4, "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:49:50.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2756,7 +2575,7 @@ "merge_request_diff_id": 27, "relative_order": 5, "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:48:32.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -2834,7 +2653,7 @@ { "merge_request_diff_id": 27, "relative_order": 5, - "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", "new_path": "files/ruby/popen.rb", "old_path": "files/ruby/popen.rb", "a_mode": "100644", @@ -2958,9 +2777,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 680, @@ -2982,9 +2799,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 681, @@ -3006,9 +2821,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 682, @@ -3030,9 +2843,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 683, @@ -3054,9 +2865,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 684, @@ -3078,9 +2887,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 685, @@ -3102,9 +2909,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 686, @@ -3126,9 +2931,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -3139,7 +2942,7 @@ "merge_request_diff_id": 26, "sha": "0b4bc9a49b562e85de7cc9e834518ea6828729b9", "relative_order": 0, - "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:26:01.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3237,9 +3040,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 778, @@ -3261,9 +3062,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 779, @@ -3285,9 +3084,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 780, @@ -3309,9 +3106,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 781, @@ -3333,9 +3128,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 782, @@ -3357,9 +3150,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 783, @@ -3381,9 +3172,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 784, @@ -3405,9 +3194,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -3516,9 +3303,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 786, @@ -3540,9 +3325,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 787, @@ -3564,9 +3347,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 788, @@ -3588,9 +3369,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 789, @@ -3612,9 +3391,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 790, @@ -3636,9 +3413,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 791, @@ -3660,9 +3435,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 792, @@ -3684,9 +3457,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -3877,7 +3648,7 @@ "merge_request_diff_id": 14, "relative_order": 15, "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T10:01:38.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3889,7 +3660,7 @@ "merge_request_diff_id": 14, "relative_order": 16, "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:57:31.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3901,7 +3672,7 @@ "merge_request_diff_id": 14, "relative_order": 17, "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:54:21.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3913,7 +3684,7 @@ "merge_request_diff_id": 14, "relative_order": 18, "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:49:50.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -3925,7 +3696,7 @@ "merge_request_diff_id": 14, "relative_order": 19, "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:48:32.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -4016,7 +3787,7 @@ { "merge_request_diff_id": 14, "relative_order": 6, - "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n", "new_path": "files/images/wm.svg", "old_path": "files/images/wm.svg", "a_mode": "0", @@ -4042,7 +3813,7 @@ { "merge_request_diff_id": 14, "relative_order": 8, - "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", "new_path": "files/ruby/popen.rb", "old_path": "files/ruby/popen.rb", "a_mode": "100644", @@ -4207,7 +3978,7 @@ }, "events": [ { - "merge_request_diff_id": 14, + "merge_request_diff_id": 14, "id": 529, "target_type": "Note", "target_id": 793, @@ -4239,9 +4010,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 795, @@ -4263,9 +4032,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 796, @@ -4287,9 +4054,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 797, @@ -4311,9 +4076,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 798, @@ -4335,9 +4098,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 799, @@ -4359,9 +4120,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 800, @@ -4383,9 +4142,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -4603,7 +4360,7 @@ { "merge_request_diff_id": 13, "relative_order": 2, - "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n", "new_path": "files/images/wm.svg", "old_path": "files/images/wm.svg", "a_mode": "0", @@ -4740,9 +4497,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 802, @@ -4764,9 +4519,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 803, @@ -4788,9 +4541,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 804, @@ -4812,9 +4563,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 805, @@ -4836,9 +4585,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 806, @@ -4860,9 +4607,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 807, @@ -4884,9 +4629,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 808, @@ -4908,9 +4651,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -5104,7 +4845,7 @@ { "merge_request_diff_id": 12, "relative_order": 2, - "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n", "new_path": "files/images/wm.svg", "old_path": "files/images/wm.svg", "a_mode": "0", @@ -5228,9 +4969,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 810, @@ -5252,9 +4991,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 811, @@ -5276,9 +5013,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 812, @@ -5300,9 +5035,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 813, @@ -5324,9 +5057,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 814, @@ -5348,9 +5079,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 815, @@ -5372,9 +5101,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 816, @@ -5396,18 +5123,14 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { "id": 11, "state": "empty", - "merge_request_diff_commits": [ - ], - "merge_request_diff_files": [ - ], + "merge_request_diff_commits": [], + "merge_request_diff_files": [], "merge_request_id": 11, "created_at": "2016-06-14T15:02:23.772Z", "updated_at": "2016-06-14T15:02:23.833Z", @@ -5482,9 +5205,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 818, @@ -5506,9 +5227,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 819, @@ -5530,9 +5249,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 820, @@ -5554,9 +5271,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 821, @@ -5578,9 +5293,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 822, @@ -5602,9 +5315,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 823, @@ -5626,9 +5337,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 824, @@ -5650,9 +5359,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -5843,7 +5550,7 @@ "merge_request_diff_id": 10, "relative_order": 16, "sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e", - "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T10:01:38.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5855,7 +5562,7 @@ "merge_request_diff_id": 10, "relative_order": 17, "sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d", - "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:57:31.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5867,7 +5574,7 @@ "merge_request_diff_id": 10, "relative_order": 18, "sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9", - "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:54:21.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5879,7 +5586,7 @@ "merge_request_diff_id": 10, "relative_order": 19, "sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08", - "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:49:50.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5891,7 +5598,7 @@ "merge_request_diff_id": 10, "relative_order": 20, "sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8", - "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n", "authored_date": "2014-02-27T09:48:32.000+01:00", "author_name": "Dmitriy Zaporozhets", "author_email": "dmitriy.zaporozhets@gmail.com", @@ -5982,7 +5689,7 @@ { "merge_request_diff_id": 10, "relative_order": 6, - "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n", + "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n", "new_path": "files/images/wm.svg", "old_path": "files/images/wm.svg", "a_mode": "0", @@ -6008,7 +5715,7 @@ { "merge_request_diff_id": 10, "relative_order": 8, - "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n", + "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n", "new_path": "files/ruby/popen.rb", "old_path": "files/ruby/popen.rb", "a_mode": "100644", @@ -6171,9 +5878,7 @@ "author": { "name": "User 4" }, - "events": [ - - ] + "events": [] }, { "id": 826, @@ -6195,9 +5900,7 @@ "author": { "name": "User 3" }, - "events": [ - - ] + "events": [] }, { "id": 827, @@ -6219,9 +5922,7 @@ "author": { "name": "User 0" }, - "events": [ - - ] + "events": [] }, { "id": 828, @@ -6243,9 +5944,7 @@ "author": { "name": "Ottis Schuster II" }, - "events": [ - - ] + "events": [] }, { "id": 829, @@ -6267,9 +5966,7 @@ "author": { "name": "Rhett Emmerich IV" }, - "events": [ - - ] + "events": [] }, { "id": 830, @@ -6291,9 +5988,7 @@ "author": { "name": "Burdette Bernier" }, - "events": [ - - ] + "events": [] }, { "id": 831, @@ -6315,9 +6010,7 @@ "author": { "name": "Ari Wintheiser" }, - "events": [ - - ] + "events": [] }, { "id": 832, @@ -6339,9 +6032,7 @@ "author": { "name": "Administrator" }, - "events": [ - - ] + "events": [] } ], "merge_request_diff": { @@ -6953,9 +6644,7 @@ "updated_at": "2017-01-16T15:25:28.637Z" } ], - "deploy_keys": [ - - ], + "deploy_keys": [], "services": [ { "id": 100, @@ -6964,9 +6653,7 @@ "created_at": "2016-06-14T15:01:51.315Z", "updated_at": "2016-06-14T15:01:51.315Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7008,9 +6695,7 @@ "created_at": "2016-06-14T15:01:51.289Z", "updated_at": "2016-06-14T15:01:51.289Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7030,9 +6715,7 @@ "created_at": "2016-06-14T15:01:51.277Z", "updated_at": "2016-06-14T15:01:51.277Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7052,9 +6735,7 @@ "created_at": "2016-06-14T15:01:51.267Z", "updated_at": "2016-06-14T15:01:51.267Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7097,9 +6778,7 @@ "created_at": "2016-06-14T15:01:51.232Z", "updated_at": "2016-06-14T15:01:51.232Z", "active": true, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7141,9 +6820,7 @@ "created_at": "2016-06-14T15:01:51.202Z", "updated_at": "2016-06-14T15:01:51.202Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7163,9 +6840,7 @@ "created_at": "2016-06-14T15:01:51.182Z", "updated_at": "2016-06-14T15:01:51.182Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7185,9 +6860,7 @@ "created_at": "2016-06-14T15:01:51.166Z", "updated_at": "2016-06-14T15:01:51.166Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7207,9 +6880,7 @@ "created_at": "2016-06-14T15:01:51.153Z", "updated_at": "2016-06-14T15:01:51.153Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7229,9 +6900,7 @@ "created_at": "2016-06-14T15:01:51.139Z", "updated_at": "2016-06-14T15:01:51.139Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7251,9 +6920,7 @@ "created_at": "2016-06-14T15:01:51.125Z", "updated_at": "2016-06-14T15:01:51.125Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7273,9 +6940,7 @@ "created_at": "2016-06-14T15:01:51.113Z", "updated_at": "2016-06-14T15:01:51.113Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7295,9 +6960,7 @@ "created_at": "2016-06-14T15:01:51.080Z", "updated_at": "2016-06-14T15:01:51.080Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7317,9 +6980,7 @@ "created_at": "2016-06-14T15:01:51.067Z", "updated_at": "2016-06-14T15:01:51.067Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7339,9 +7000,7 @@ "created_at": "2016-06-14T15:01:51.047Z", "updated_at": "2016-06-14T15:01:51.047Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7361,9 +7020,7 @@ "created_at": "2016-06-14T15:01:51.031Z", "updated_at": "2016-06-14T15:01:51.031Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7383,9 +7040,7 @@ "created_at": "2016-06-14T15:01:51.031Z", "updated_at": "2016-06-14T15:01:51.031Z", "active": false, - "properties": { - - }, + "properties": {}, "template": false, "push_events": true, "issues_events": true, @@ -7399,9 +7054,7 @@ "type": "JenkinsDeprecatedService" } ], - "hooks": [ - - ], + "hooks": [], "protected_branches": [ { "id": 1, @@ -7475,5 +7128,25 @@ "key": "bar", "value": "bar" } + ], + "project_badges": [ + { + "id": 1, + "created_at": "2017-10-19T15:36:23.466Z", + "updated_at": "2017-10-19T15:36:23.466Z", + "project_id": 5, + "type": "ProjectBadge", + "link_url": "http://www.example.com", + "image_url": "http://www.example.com" + }, + { + "id": 2, + "created_at": "2017-10-19T15:36:23.466Z", + "updated_at": "2017-10-19T15:36:23.466Z", + "project_id": 5, + "type": "ProjectBadge", + "link_url": "http://www.example.com", + "image_url": "http://www.example.com" + } ] } diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index d076007e4bc..1a4d09724fc 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -129,6 +129,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(@project.custom_attributes.count).to eq(2) end + it 'has badges' do + expect(@project.project_badges.count).to eq(2) + end + it 'restores the correct service' do expect(CustomIssueTrackerService.first).not_to be_nil end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 5804c45871e..d6bd5f5c81d 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -180,6 +180,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['custom_attributes'].count).to eq(2) end + it 'has badges' do + expect(saved_project_json['project_badges'].count).to eq(2) + end + it 'does not complain about non UTF-8 characters in MR diff files' do ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") @@ -288,6 +292,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver do create(:project_custom_attribute, project: project) create(:project_custom_attribute, project: project) + create(:project_badge, project: project) + create(:project_badge, project: project) + project end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index feaab6673cd..ddcbb7a0033 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -536,3 +536,12 @@ LfsFileLock: - user_id - project_id - created_at +Badge: +- id +- link_url +- image_url +- project_id +- group_id +- created_at +- updated_at +- type diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb new file mode 100644 index 00000000000..33dfa461202 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes::ConfigMap do + let(:kubeclient) { double('kubernetes client') } + let(:application) { create(:clusters_applications_prometheus) } + let(:config_map) { described_class.new(application.name, application.values) } + let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } + + let(:metadata) do + { + name: "values-content-configuration-#{application.name}", + namespace: namespace, + labels: { name: "values-content-configuration-#{application.name}" } + } + end + + describe '#generate' do + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: { values: application.values }) } + subject { config_map.generate } + + it 'should build a Kubeclient Resource' do + is_expected.to eq(resource) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 69112fe90b1..740466ea5cb 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -5,14 +5,21 @@ describe Gitlab::Kubernetes::Helm::Api do let(:helm) { described_class.new(client) } let(:gitlab_namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) } - let(:install_helm) { true } - let(:chart) { 'stable/a_chart' } - let(:application_name) { 'app_name' } - let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm: install_helm, chart: chart) } + let(:application) { create(:clusters_applications_prometheus) } + + let(:command) do + Gitlab::Kubernetes::Helm::InstallCommand.new( + application.name, + chart: application.chart, + values: application.values + ) + end + subject { helm } before do allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client).and_return(namespace) + allow(client).to receive(:create_config_map) end describe '#initialize' do @@ -26,6 +33,7 @@ describe Gitlab::Kubernetes::Helm::Api do describe '#install' do before do allow(client).to receive(:create_pod).and_return(nil) + allow(client).to receive(:create_config_map).and_return(nil) allow(namespace).to receive(:ensure_exists!).once end @@ -35,6 +43,16 @@ describe Gitlab::Kubernetes::Helm::Api do subject.install(command) end + + context 'with a ConfigMap' do + let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application.name, application.values).generate } + + it 'creates a ConfigMap on kubeclient' do + expect(client).to receive(:create_config_map).with(resource).once + + subject.install(command) + end + end end describe '#installation_status' do diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb new file mode 100644 index 00000000000..3cfdae794f6 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes::Helm::BaseCommand do + let(:application) { create(:clusters_applications_helm) } + let(:base_command) { described_class.new(application.name) } + + describe '#generate_script' do + let(:helm_version) { Gitlab::Kubernetes::Helm::HELM_VERSION } + let(:command) do + <<~HEREDOC + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{helm_version}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + HEREDOC + end + + subject { base_command.generate_script } + + it 'should return a command that prepares the environment for helm-cli' do + expect(subject).to eq(command) + end + end + + describe '#pod_resource' do + subject { base_command.pod_resource } + + it 'should returns a kubeclient resoure with pod content for application' do + is_expected.to be_an_instance_of ::Kubeclient::Resource + end + end + + describe '#config_map?' do + subject { base_command.config_map? } + + it { is_expected.to be_falsy } + end + + describe '#pod_name' do + subject { base_command.pod_name } + + it { is_expected.to eq('install-helm') } + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb new file mode 100644 index 00000000000..e6920b0a76f --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes::Helm::InitCommand do + let(:application) { create(:clusters_applications_helm) } + let(:init_command) { described_class.new(application.name) } + + describe '#generate_script' do + let(:command) do + <<~MSG.chomp + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + helm init >/dev/null + MSG + end + + subject { init_command.generate_script } + + it 'should return the appropriate command' do + is_expected.to eq(command) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 63997a40d52..137b8f718de 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -1,79 +1,56 @@ require 'rails_helper' describe Gitlab::Kubernetes::Helm::InstallCommand do - let(:prometheus) { create(:clusters_applications_prometheus) } - - describe "#initialize" do - context "With all the params" do - subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) } - - it 'should assign all parameters' do - expect(subject.name).to eq(prometheus.name) - expect(subject.install_helm).to be_truthy - expect(subject.chart).to eq(prometheus.chart) - expect(subject.chart_values_file).to eq("#{Rails.root}/vendor/prometheus/values.yaml") - end - end - - context 'when install_helm is not set' do - subject { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: true) } - - it 'should set install_helm as false' do - expect(subject.install_helm).to be_falsy - end - end - - context 'when chart is not set' do - subject { described_class.new(prometheus.name, install_helm: true) } + let(:application) { create(:clusters_applications_prometheus) } + let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } + + let(:install_command) do + described_class.new( + application.name, + chart: application.chart, + values: application.values + ) + end - it 'should set chart as nil' do - expect(subject.chart).to be_falsy - end + describe '#generate_script' do + let(:command) do + <<~MSG + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + helm init --client-only >/dev/null + helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null + MSG end - context 'when chart_values_file is not set' do - subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart) } + subject { install_command.generate_script } - it 'should set chart_values_file as nil' do - expect(subject.chart_values_file).to be_falsy - end + it 'should return appropriate command' do + is_expected.to eq(command) end - end - - describe "#generate_script" do - let(:install_command) { described_class.new(prometheus.name, install_helm: install_helm) } - let(:client) { double('kubernetes client') } - let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) } - subject { install_command.send(:generate_script, namespace.name) } - context 'when install helm is true' do - let(:install_helm) { true } - let(:command) do - <<~MSG - set -eo pipefail - apk add -U ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ - - helm init >/dev/null - MSG + context 'with an application with a repository' do + let(:ci_runner) { create(:ci_runner) } + let(:application) { create(:clusters_applications_runner, runner: ci_runner) } + let(:install_command) do + described_class.new( + application.name, + chart: application.chart, + values: application.values, + repository: application.repository + ) end - it 'should return appropriate command' do - is_expected.to eq(command) - end - end - - context 'when install helm is false' do - let(:install_helm) { false } let(:command) do <<~MSG set -eo pipefail apk add -U ca-certificates openssl >/dev/null wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null mv /tmp/linux-amd64/helm /usr/bin/ - helm init --client-only >/dev/null + helm repo add #{application.name} #{application.repository} + helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null MSG end @@ -81,50 +58,29 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do is_expected.to eq(command) end end + end - context 'when chart is present' do - let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart) } - let(:command) do - <<~MSG.chomp - set -eo pipefail - apk add -U ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ + describe '#config_map?' do + subject { install_command.config_map? } - helm init --client-only >/dev/null - helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} >/dev/null - MSG - end + it { is_expected.to be_truthy } + end - it 'should return appropriate command' do - is_expected.to eq(command) - end + describe '#config_map_resource' do + let(:metadata) do + { + name: "values-content-configuration-#{application.name}", + namespace: namespace, + labels: { name: "values-content-configuration-#{application.name}" } + } end - context 'when chart values file is present' do - let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) } - let(:command) do - <<~MSG.chomp - set -eo pipefail - apk add -U ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: { values: application.values }) } - helm init --client-only >/dev/null - helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} -f /data/helm/#{prometheus.name}/config/values.yaml >/dev/null - MSG - end + subject { install_command.config_map_resource } - it 'should return appropriate command' do - is_expected.to eq(command) - end + it 'returns a KubeClient resource with config map content for the application' do + is_expected.to eq(resource) end end - - describe "#pod_name" do - let(:install_command) { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: true) } - subject { install_command.send(:pod_name) } - - it { is_expected.to eq('install-prometheus') } - end end diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index ebb6033f71e..43adc80d576 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -5,13 +5,9 @@ describe Gitlab::Kubernetes::Helm::Pod do let(:cluster) { create(:cluster) } let(:app) { create(:clusters_applications_prometheus, cluster: cluster) } let(:command) { app.install_command } - let(:client) { double('kubernetes client') } - let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) } - subject { described_class.new(command, namespace.name, client) } + let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } - before do - allow(client).to receive(:create_config_map).and_return(nil) - end + subject { described_class.new(command, namespace) } shared_examples 'helm pod' do it 'should generate a Kubeclient::Resource' do @@ -47,7 +43,7 @@ describe Gitlab::Kubernetes::Helm::Pod do end end - context 'with a configuration file' do + context 'with a install command' do it_behaves_like 'helm pod' it 'should include volumes for the container' do @@ -62,14 +58,14 @@ describe Gitlab::Kubernetes::Helm::Pod do end it 'should mount configMap specification in the volume' do - spec = subject.generate.spec - expect(spec.volumes.first.configMap['name']).to eq("values-content-configuration-#{app.name}") - expect(spec.volumes.first.configMap['items'].first['key']).to eq('values') - expect(spec.volumes.first.configMap['items'].first['path']).to eq('values.yaml') + volume = subject.generate.spec.volumes.first + expect(volume.configMap['name']).to eq("values-content-configuration-#{app.name}") + expect(volume.configMap['items'].first['key']).to eq('values') + expect(volume.configMap['items'].first['path']).to eq('values.yaml') end end - context 'without a configuration file' do + context 'with a init command' do let(:app) { create(:clusters_applications_helm, cluster: cluster) } it_behaves_like 'helm pod' diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index 07ba11b93a3..39ec2f37a83 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -11,15 +11,17 @@ describe Gitlab::Middleware::ReadOnly do RSpec::Matchers.define :disallow_request do match do |middleware| - flash = middleware.send(:rack_flash) - flash['alert'] && flash['alert'].include?('You cannot do writing operations') + alert = middleware.env['rack.session'].to_hash + .dig('flash', 'flashes', 'alert') + + alert&.include?('You cannot perform write operations') end end RSpec::Matchers.define :disallow_request_in_json do match do |response| json_response = JSON.parse(response.body) - response.body.include?('You cannot do writing operations') && json_response.key?('message') + response.body.include?('You cannot perform write operations') && json_response.key?('message') end end @@ -34,10 +36,25 @@ describe Gitlab::Middleware::ReadOnly do rack.to_app end - subject { described_class.new(fake_app) } + let(:observe_env) do + Module.new do + attr_reader :env + + def call(env) + @env = env + super + end + end + end let(:request) { Rack::MockRequest.new(rack_stack) } + subject do + described_class.new(fake_app).tap do |app| + app.extend(observe_env) + end + end + context 'normal requests to a read-only Gitlab instance' do let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } diff --git a/spec/lib/gitlab/middleware/release_env_spec.rb b/spec/lib/gitlab/middleware/release_env_spec.rb new file mode 100644 index 00000000000..5e3aa877409 --- /dev/null +++ b/spec/lib/gitlab/middleware/release_env_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::Middleware::ReleaseEnv do + let(:inner_app) { double(:app, call: 'yay') } + let(:app) { described_class.new(inner_app) } + let(:env) { { 'action_controller.instance' => 'something' } } + + describe '#call' do + it 'calls the app and clears the env' do + result = app.call(env) + + expect(result).to eq('yay') + expect(env).to be_empty + end + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index d8250e4b4c6..c46bb8edebf 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -217,7 +217,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).not_to include security_issue_1 expect(issues).not_to include security_issue_2 - expect(results.issues_count).to eq 1 + expect(results.limited_issues_count).to eq 1 end it 'does not list project confidential issues for project members with guest role' do @@ -229,7 +229,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).not_to include security_issue_1 expect(issues).not_to include security_issue_2 - expect(results.issues_count).to eq 1 + expect(results.limited_issues_count).to eq 1 end it 'lists project confidential issues for author' do @@ -239,7 +239,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).to include security_issue_1 expect(issues).not_to include security_issue_2 - expect(results.issues_count).to eq 2 + expect(results.limited_issues_count).to eq 2 end it 'lists project confidential issues for assignee' do @@ -249,7 +249,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).not_to include security_issue_1 expect(issues).to include security_issue_2 - expect(results.issues_count).to eq 2 + expect(results.limited_issues_count).to eq 2 end it 'lists project confidential issues for project members' do @@ -261,7 +261,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).to include security_issue_1 expect(issues).to include security_issue_2 - expect(results.issues_count).to eq 3 + expect(results.limited_issues_count).to eq 3 end it 'lists all project issues for admin' do @@ -271,7 +271,7 @@ describe Gitlab::ProjectSearchResults do expect(issues).to include issue expect(issues).to include security_issue_1 expect(issues).to include security_issue_2 - expect(results.issues_count).to eq 3 + expect(results.limited_issues_count).to eq 3 end end @@ -304,6 +304,35 @@ describe Gitlab::ProjectSearchResults do end end + describe '#limited_notes_count' do + let(:project) { create(:project, :public) } + let(:note) { create(:note_on_issue, project: project) } + let(:results) { described_class.new(user, project, note.note) } + + context 'when count_limit is lower than total amount' do + before do + allow(results).to receive(:count_limit).and_return(1) + end + + it 'calls note finder once to get the limited amount of notes' do + expect(results).to receive(:notes_finder).once.and_call_original + expect(results.limited_notes_count).to eq(1) + end + end + + context 'when count_limit is higher than total amount' do + it 'calls note finder multiple times to get the limited amount of notes' do + project = create(:project, :public) + note = create(:note_on_issue, project: project) + + results = described_class.new(user, project, note.note) + + expect(results).to receive(:notes_finder).exactly(4).times.and_call_original + expect(results.limited_notes_count).to eq(1) + end + end + end + # Examples for commit access level test # # params: diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 9dbab95f70e..87288baedb0 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -29,30 +29,6 @@ describe Gitlab::SearchResults do end end - describe '#projects_count' do - it 'returns the total amount of projects' do - expect(results.projects_count).to eq(1) - end - end - - describe '#issues_count' do - it 'returns the total amount of issues' do - expect(results.issues_count).to eq(1) - end - end - - describe '#merge_requests_count' do - it 'returns the total amount of merge requests' do - expect(results.merge_requests_count).to eq(1) - end - end - - describe '#milestones_count' do - it 'returns the total amount of milestones' do - expect(results.milestones_count).to eq(1) - end - end - context "when count_limit is lower than total amount" do before do allow(results).to receive(:count_limit).and_return(1) @@ -183,7 +159,7 @@ describe Gitlab::SearchResults do expect(issues).not_to include security_issue_3 expect(issues).not_to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 1 + expect(results.limited_issues_count).to eq 1 end it 'does not list confidential issues for project members with guest role' do @@ -199,7 +175,7 @@ describe Gitlab::SearchResults do expect(issues).not_to include security_issue_3 expect(issues).not_to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 1 + expect(results.limited_issues_count).to eq 1 end it 'lists confidential issues for author' do @@ -212,7 +188,7 @@ describe Gitlab::SearchResults do expect(issues).to include security_issue_3 expect(issues).not_to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 3 + expect(results.limited_issues_count).to eq 3 end it 'lists confidential issues for assignee' do @@ -225,7 +201,7 @@ describe Gitlab::SearchResults do expect(issues).not_to include security_issue_3 expect(issues).to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 3 + expect(results.limited_issues_count).to eq 3 end it 'lists confidential issues for project members' do @@ -241,7 +217,7 @@ describe Gitlab::SearchResults do expect(issues).to include security_issue_3 expect(issues).not_to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 4 + expect(results.limited_issues_count).to eq 4 end it 'lists all issues for admin' do @@ -254,7 +230,7 @@ describe Gitlab::SearchResults do expect(issues).to include security_issue_3 expect(issues).to include security_issue_4 expect(issues).not_to include security_issue_5 - expect(results.issues_count).to eq 5 + expect(results.limited_issues_count).to eq 5 end end diff --git a/spec/lib/gitlab/string_placeholder_replacer_spec.rb b/spec/lib/gitlab/string_placeholder_replacer_spec.rb new file mode 100644 index 00000000000..7a03ea4154c --- /dev/null +++ b/spec/lib/gitlab/string_placeholder_replacer_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::StringPlaceholderReplacer do + describe '.render_url' do + it 'returns the nil if the string is blank' do + expect(described_class.replace_string_placeholders(nil, /whatever/)).to be_blank + end + + it 'returns the string if the placeholder regex' do + expect(described_class.replace_string_placeholders('whatever')).to eq 'whatever' + end + + it 'returns the string if no block given' do + expect(described_class.replace_string_placeholders('whatever', /whatever/)).to eq 'whatever' + end + + context 'when all params are valid' do + let(:string) { '%{path}/%{id}/%{branch}' } + let(:regex) { /(path|id)/ } + + it 'replaces each placeholders with the block result' do + result = described_class.replace_string_placeholders(string, regex) do |arg| + 'WHATEVER' + end + + expect(result).to eq 'WHATEVER/WHATEVER/%{branch}' + end + + it 'does not replace the placeholder if the block result is nil' do + result = described_class.replace_string_placeholders(string, regex) do |arg| + arg == 'path' ? nil : 'WHATEVER' + end + + expect(result).to eq '%{path}/WHATEVER/%{branch}' + end + end + end +end diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb index d715f9bd641..37b1298b962 100644 --- a/spec/lib/gitlab/string_regex_marker_spec.rb +++ b/spec/lib/gitlab/string_regex_marker_spec.rb @@ -2,17 +2,36 @@ require 'spec_helper' describe Gitlab::StringRegexMarker do describe '#mark' do - let(:raw) { %{"name": "AFNetworking"} } - let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe } - subject do - described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:| - %{<a href="#">#{text}</a>} + context 'with a single occurrence' do + let(:raw) { %{"name": "AFNetworking"} } + let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe } + + subject do + described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:| + %{<a href="#">#{text}</a>} + end + end + + it 'marks the match' do + expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>}) + expect(subject).to be_html_safe end end - it 'marks the inline diffs' do - expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>}) - expect(subject).to be_html_safe + context 'with multiple occurrences' do + let(:raw) { %{a <b> <c> d} } + let(:rich) { %{a <b> <c> d}.html_safe } + + subject do + described_class.new(raw, rich).mark(/<[a-z]>/) do |text, left:, right:| + %{<strong>#{text}</strong>} + end + end + + it 'marks the matches' do + expect(subject).to eq(%{a <strong><b></strong> <strong><c></strong> d}) + expect(subject).to be_html_safe + end end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 0e9ecff25a6..138d21ede97 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -36,6 +36,7 @@ describe Gitlab::UsageData do gitlab_shared_runners git database + avg_cycle_analytics )) end diff --git a/spec/lib/gitlab/verify/lfs_objects_spec.rb b/spec/lib/gitlab/verify/lfs_objects_spec.rb new file mode 100644 index 00000000000..64f3a9660e0 --- /dev/null +++ b/spec/lib/gitlab/verify/lfs_objects_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::Verify::LfsObjects do + include GitlabVerifyHelpers + + it_behaves_like 'Gitlab::Verify::BatchVerifier subclass' do + let!(:objects) { create_list(:lfs_object, 3, :with_file) } + end + + describe '#run_batches' do + let(:failures) { collect_failures } + let(:failure) { failures[lfs_object] } + + let!(:lfs_object) { create(:lfs_object, :with_file, :correct_oid) } + + it 'passes LFS objects with the correct file' do + expect(failures).to eq({}) + end + + it 'fails LFS objects with a missing file' do + FileUtils.rm_f(lfs_object.file.path) + + expect(failures.keys).to contain_exactly(lfs_object) + expect(failure).to be_a(Errno::ENOENT) + expect(failure.to_s).to include(lfs_object.file.path) + end + + it 'fails LFS objects with a mismatched oid' do + File.truncate(lfs_object.file.path, 0) + + expect(failures.keys).to contain_exactly(lfs_object) + expect(failure.to_s).to include('Checksum mismatch') + end + end +end diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb new file mode 100644 index 00000000000..6146ce61226 --- /dev/null +++ b/spec/lib/gitlab/verify/uploads_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Gitlab::Verify::Uploads do + include GitlabVerifyHelpers + + it_behaves_like 'Gitlab::Verify::BatchVerifier subclass' do + let(:projects) { create_list(:project, 3, :with_avatar) } + let!(:objects) { projects.flat_map(&:uploads) } + end + + describe '#run_batches' do + let(:project) { create(:project, :with_avatar) } + let(:failures) { collect_failures } + let(:failure) { failures[upload] } + + let!(:upload) { project.uploads.first } + + it 'passes uploads with the correct file' do + expect(failures).to eq({}) + end + + it 'fails uploads with a missing file' do + FileUtils.rm_f(upload.absolute_path) + + expect(failures.keys).to contain_exactly(upload) + expect(failure).to be_a(Errno::ENOENT) + expect(failure.to_s).to include(upload.absolute_path) + end + + it 'fails uploads with a mismatched checksum' do + upload.update!(checksum: 'something incorrect') + + expect(failures.keys).to contain_exactly(upload) + expect(failure.to_s).to include('Checksum mismatch') + end + + it 'fails uploads with a missing precalculated checksum' do + upload.update!(checksum: '') + + expect(failures.keys).to contain_exactly(upload) + expect(failure.to_s).to include('Checksum missing') + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index bcbb9287199..83c33797bbc 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -457,7 +457,7 @@ describe Notify do it 'has the correct subject and body' do is_expected.to have_subject("#{project.name} | Project was moved") - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text(project.ssh_url_to_repo) end end @@ -483,8 +483,8 @@ describe Notify do to_emails = subject.header[:to].addrs.map(&:address) expect(to_emails).to eq([recipient.notification_email]) - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_subject "Request to join the #{project.full_name} project" + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project_project_members_url(project) is_expected.to have_body_text project_member.human_access end @@ -503,8 +503,8 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do - is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied" - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_subject "Access to the #{project.full_name} project was denied" + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url end end @@ -520,8 +520,8 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do - is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted" - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_subject "Access to the #{project.full_name} project was granted" + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.human_access end @@ -550,8 +550,8 @@ describe Notify do it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do - is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_subject "Invitation to join the #{project.full_name} project" + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.human_access is_expected.to have_body_text project_member.invite_token @@ -575,7 +575,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.invite_email is_expected.to have_html_escaped_body_text invited_user.name @@ -598,7 +598,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_html_escaped_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.invite_email end diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb new file mode 100644 index 00000000000..33dc19e3432 --- /dev/null +++ b/spec/models/badge_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Badge do + let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' } + + describe 'validations' do + # Requires the let variable url_sym + shared_examples 'placeholder url' do + let(:badge) { build(:badge) } + + it 'allows url with http protocol' do + badge[url_sym] = 'http://www.example.com' + + expect(badge).to be_valid + end + + it 'allows url with https protocol' do + badge[url_sym] = 'https://www.example.com' + + expect(badge).to be_valid + end + + it 'cannot be empty' do + badge[url_sym] = '' + + expect(badge).not_to be_valid + end + + it 'cannot be nil' do + badge[url_sym] = nil + + expect(badge).not_to be_valid + end + + it 'accept badges placeholders' do + badge[url_sym] = placeholder_url + + expect(badge).to be_valid + end + + it 'sanitize url' do + badge[url_sym] = 'javascript:alert(1)' + + expect(badge).not_to be_valid + end + end + + context 'link_url format' do + let(:url_sym) { :link_url } + + it_behaves_like 'placeholder url' + end + + context 'image_url format' do + let(:url_sym) { :image_url } + + it_behaves_like 'placeholder url' + end + end + + shared_examples 'rendered_links' do + it 'should use the project information to populate the url placeholders' do + stub_project_commit_info(project) + + expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever" + end + + it 'returns the url if the project used is nil' do + expect(badge.public_send("rendered_#{method}", nil)).to eq placeholder_url + end + + def stub_project_commit_info(project) + allow(project).to receive(:commit).and_return(double('Commit', sha: 'whatever')) + allow(project).to receive(:default_branch).and_return('master') + end + end + + context 'methods' do + let(:badge) { build(:badge, link_url: placeholder_url, image_url: placeholder_url) } + let!(:project) { create(:project) } + + context '#rendered_link_url' do + let(:method) { :link_url } + + it_behaves_like 'rendered_links' + end + + context '#rendered_image_url' do + let(:method) { :image_url } + + it_behaves_like 'rendered_links' + end + end +end diff --git a/spec/models/badges/group_badge_spec.rb b/spec/models/badges/group_badge_spec.rb new file mode 100644 index 00000000000..ed7f83d0489 --- /dev/null +++ b/spec/models/badges/group_badge_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe GroupBadge do + describe 'associations' do + it { is_expected.to belong_to(:group) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:group) } + end +end diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb new file mode 100644 index 00000000000..0e1a8159cb6 --- /dev/null +++ b/spec/models/badges/project_badge_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe ProjectBadge do + let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' } + + describe 'associations' do + it { is_expected.to belong_to(:project) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + end + + shared_examples 'rendered_links' do + it 'should use the badge project information to populate the url placeholders' do + stub_project_commit_info(project) + + expect(badge.public_send("rendered_#{method}")).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever" + end + + def stub_project_commit_info(project) + allow(project).to receive(:commit).and_return(double('Commit', sha: 'whatever')) + allow(project).to receive(:default_branch).and_return('master') + end + end + + context 'methods' do + let(:badge) { build(:project_badge, link_url: placeholder_url, image_url: placeholder_url) } + let!(:project) { badge.project } + + context '#rendered_link_url' do + let(:method) { :link_url } + + it_behaves_like 'rendered_links' + end + + context '#rendered_image_url' do + let(:method) { :image_url } + + it_behaves_like 'rendered_links' + end + end +end diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index eb57abaf6ef..ba7bad617b4 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -1,102 +1,17 @@ require 'rails_helper' describe Clusters::Applications::Helm do - it { is_expected.to belong_to(:cluster) } - it { is_expected.to validate_presence_of(:cluster) } - - describe '#name' do - it 'is .application_name' do - expect(subject.name).to eq(described_class.application_name) - end - - it 'is recorded in Clusters::Cluster::APPLICATIONS' do - expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class) - end - end - - describe '#version' do - it 'defaults to Gitlab::Kubernetes::Helm::HELM_VERSION' do - expect(subject.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) - end - end - - describe '#status' do - let(:cluster) { create(:cluster) } - - subject { described_class.new(cluster: cluster) } - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - - context 'when platform kubernetes is defined' do - let(:cluster) { create(:cluster, :provided_by_gcp) } - - it 'defaults to :installable' do - expect(subject.status_name).to be(:installable) - end - end - end + include_examples 'cluster application core specs', :clusters_applications_helm describe '#install_command' do - it 'has all the needed information' do - expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true) - end - end - - describe 'status state machine' do - describe '#make_installing' do - subject { create(:clusters_applications_helm, :scheduled) } - - it 'is installing' do - subject.make_installing! - - expect(subject).to be_installing - end - end - - describe '#make_installed' do - subject { create(:clusters_applications_helm, :installing) } - - it 'is installed' do - subject.make_installed - - expect(subject).to be_installed - end - end - - describe '#make_errored' do - subject { create(:clusters_applications_helm, :installing) } - let(:reason) { 'some errors' } - - it 'is errored' do - subject.make_errored(reason) - - expect(subject).to be_errored - expect(subject.status_reason).to eq(reason) - end - end - - describe '#make_scheduled' do - subject { create(:clusters_applications_helm, :installable) } - - it 'is scheduled' do - subject.make_scheduled - - expect(subject).to be_scheduled - end - - describe 'when was errored' do - subject { create(:clusters_applications_helm, :errored) } + let(:helm) { create(:clusters_applications_helm) } - it 'clears #status_reason' do - expect(subject.status_reason).not_to be_nil + subject { helm.install_command } - subject.make_scheduled! + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InitCommand) } - expect(subject.status_reason).to be_nil - end - end + it 'should be initialized with 1 arguments' do + expect(subject.name).to eq('helm') end end end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 619c088b0bf..03f5b88a525 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -1,8 +1,78 @@ require 'rails_helper' describe Clusters::Applications::Ingress do - it { is_expected.to belong_to(:cluster) } - it { is_expected.to validate_presence_of(:cluster) } + let(:ingress) { create(:clusters_applications_ingress) } - include_examples 'cluster application specs', described_class + include_examples 'cluster application core specs', :clusters_applications_ingress + include_examples 'cluster application status specs', :cluster_application_ingress + + before do + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) + allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) + end + + describe '#make_installed!' do + before do + application.make_installed! + end + + let(:application) { create(:clusters_applications_ingress, :installing) } + + it 'schedules a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_in) + .with(Clusters::Applications::Ingress::FETCH_IP_ADDRESS_DELAY, 'ingress', application.id) + end + end + + describe '#schedule_status_update' do + let(:application) { create(:clusters_applications_ingress, :installed) } + + before do + application.schedule_status_update + end + + it 'schedules a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_async) + .with('ingress', application.id) + end + + context 'when the application is not installed' do + let(:application) { create(:clusters_applications_ingress, :installing) } + + it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_async) + end + end + + context 'when there is already an external_ip' do + let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '111.222.222.111') } + + it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do + expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in) + end + end + end + + describe '#install_command' do + subject { ingress.install_command } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + + it 'should be initialized with ingress arguments' do + expect(subject.name).to eq('ingress') + expect(subject.chart).to eq('stable/nginx-ingress') + expect(subject.values).to eq(ingress.values) + end + end + + describe '#values' do + subject { ingress.values } + + it 'should include ingress valid keys' do + is_expected.to include('image') + is_expected.to include('repository') + is_expected.to include('stats') + is_expected.to include('podAnnotations') + end + end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 01037919530..df8a508e021 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -1,10 +1,8 @@ require 'rails_helper' describe Clusters::Applications::Prometheus do - it { is_expected.to belong_to(:cluster) } - it { is_expected.to validate_presence_of(:cluster) } - - include_examples 'cluster application specs', described_class + include_examples 'cluster application core specs', :clusters_applications_prometheus + include_examples 'cluster application status specs', :cluster_application_prometheus describe 'transition to installed' do let(:project) { create(:project) } @@ -24,14 +22,6 @@ describe Clusters::Applications::Prometheus do end end - describe "#chart_values_file" do - subject { create(:clusters_applications_prometheus).chart_values_file } - - it 'should return chart values file path' do - expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml") - end - end - describe '#proxy_client' do context 'cluster is nil' do it 'returns nil' do @@ -85,4 +75,33 @@ describe Clusters::Applications::Prometheus do end end end + + describe '#install_command' do + let(:kubeclient) { double('kubernetes client') } + let(:prometheus) { create(:clusters_applications_prometheus) } + + subject { prometheus.install_command } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + + it 'should be initialized with 3 arguments' do + expect(subject.name).to eq('prometheus') + expect(subject.chart).to eq('stable/prometheus') + expect(subject.values).to eq(prometheus.values) + end + end + + describe '#values' do + let(:prometheus) { create(:clusters_applications_prometheus) } + + subject { prometheus.values } + + it 'should include prometheus valid values' do + is_expected.to include('alertmanager') + is_expected.to include('kubeStateMetrics') + is_expected.to include('nodeExporter') + is_expected.to include('pushgateway') + is_expected.to include('serverFiles') + end + end end diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb new file mode 100644 index 00000000000..a574779e39d --- /dev/null +++ b/spec/models/clusters/applications/runner_spec.rb @@ -0,0 +1,99 @@ +require 'rails_helper' + +describe Clusters::Applications::Runner do + let(:ci_runner) { create(:ci_runner) } + + include_examples 'cluster application core specs', :clusters_applications_runner + include_examples 'cluster application status specs', :cluster_application_runner + + it { is_expected.to belong_to(:runner) } + + describe '#install_command' do + let(:kubeclient) { double('kubernetes client') } + let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } + + subject { gitlab_runner.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('runner') + expect(subject.chart).to eq('runner/gitlab-runner') + expect(subject.repository).to eq('https://charts.gitlab.io') + expect(subject.values).to eq(gitlab_runner.values) + end + end + + describe '#values' do + let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } + + subject { gitlab_runner.values } + + it 'should include runner valid values' do + is_expected.to include('concurrent') + is_expected.to include('checkInterval') + is_expected.to include('rbac') + is_expected.to include('runners') + is_expected.to include('privileged: true') + is_expected.to include('image: ubuntu:16.04') + is_expected.to include('resources') + is_expected.to include("runnerToken: #{ci_runner.token}") + is_expected.to include("gitlabUrl: #{Gitlab::Routing.url_helpers.root_url}") + end + + context 'without a runner' do + let(:project) { create(:project) } + let(:cluster) { create(:cluster) } + let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) } + + before do + cluster.projects << project + end + + it 'creates a runner' do + expect do + subject + end.to change { Ci::Runner.count }.by(1) + end + + it 'uses the new runner token' do + expect(subject).to include("runnerToken: #{gitlab_runner.reload.runner.token}") + end + + it 'assigns the new runner to runner' do + subject + gitlab_runner.reload + + expect(gitlab_runner.runner).not_to be_nil + end + end + + context 'with duplicated values on vendor/runner/values.yaml' do + let(:values) do + { + "concurrent" => 4, + "checkInterval" => 3, + "rbac" => { + "create" => false + }, + "clusterWideAccess" => false, + "runners" => { + "privileged" => false, + "image" => "ubuntu:16.04", + "builds" => {}, + "services" => {}, + "helpers" => {} + } + } + end + + before do + allow(gitlab_runner).to receive(:chart_values).and_return(values) + end + + it 'should overwrite values.yaml' do + is_expected.to include("privileged: #{gitlab_runner.privileged}") + end + end + end +end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 799d7ced116..8f12a0e3085 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -8,6 +8,7 @@ describe Clusters::Cluster do it { is_expected.to have_one(:application_helm) } it { is_expected.to have_one(:application_ingress) } it { is_expected.to have_one(:application_prometheus) } + it { is_expected.to have_one(:application_runner) } it { is_expected.to delegate_method(:status).to(:provider) } it { is_expected.to delegate_method(:status_reason).to(:provider) } it { is_expected.to delegate_method(:status_name).to(:provider) } @@ -196,9 +197,10 @@ describe Clusters::Cluster do let!(:helm) { create(:clusters_applications_helm, cluster: cluster) } let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) } let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) } + let!(:runner) { create(:clusters_applications_runner, cluster: cluster) } it 'returns a list of created applications' do - is_expected.to contain_exactly(helm, ingress, prometheus) + is_expected.to contain_exactly(helm, ingress, prometheus, runner) end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index c536dab2681..b7ed8be69fc 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -368,7 +368,9 @@ describe CommitStatus do 'rspec:windows 0 : / 1' => 'rspec:windows', 'rspec:windows 0 : / 1 name' => 'rspec:windows name', '0 1 name ruby' => 'name ruby', - '0 :/ 1 name ruby' => 'name ruby' + '0 :/ 1 name ruby' => 'name ruby', + 'golang test 1.8' => 'golang test', + '1.9 golang test' => 'golang test' } tests.each do |name, group_name| diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index f2f1928926c..6a6b58fb52b 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -18,11 +18,11 @@ describe 'CycleAnalytics#code' do end]], end_time_conditions: [["merge request that closes issue is created", -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) + context.create_merge_request_closing_issue(context.user, context.project, data[:issue]) end]], post_fn: -> (context, data) do - context.merge_merge_requests_closing_issue(data[:issue]) - context.deploy_master + context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) + context.deploy_master(context.user, context.project) end) context "when a regular merge request (that doesn't close the issue) is created" do @@ -30,10 +30,10 @@ describe 'CycleAnalytics#code' do issue = create(:issue, project: project) create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_merge_request_closing_issue(user, project, issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - deploy_master + merge_merge_requests_closing_issue(user, project, issue) + deploy_master(user, project) expect(subject[:code].median).to be_nil end @@ -50,10 +50,10 @@ describe 'CycleAnalytics#code' do end]], end_time_conditions: [["merge request that closes issue is created", -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) + context.create_merge_request_closing_issue(context.user, context.project, data[:issue]) end]], post_fn: -> (context, data) do - context.merge_merge_requests_closing_issue(data[:issue]) + context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) end) context "when a regular merge request (that doesn't close the issue) is created" do @@ -61,9 +61,9 @@ describe 'CycleAnalytics#code' do issue = create(:issue, project: project) create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_merge_request_closing_issue(user, project, issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) + merge_merge_requests_closing_issue(user, project, issue) expect(subject[:code].median).to be_nil end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 985e1bf80be..45f1b4fe8a3 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -26,8 +26,8 @@ describe 'CycleAnalytics#issue' do end]], post_fn: -> (context, data) do if data[:issue].persisted? - context.create_merge_request_closing_issue(data[:issue].reload) - context.merge_merge_requests_closing_issue(data[:issue]) + context.create_merge_request_closing_issue(context.user, context.project, data[:issue].reload) + context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) end end) @@ -37,8 +37,8 @@ describe 'CycleAnalytics#issue' do issue = create(:issue, project: project) issue.update(label_ids: [regular_label.id]) - create_merge_request_closing_issue(issue) - merge_merge_requests_closing_issue(issue) + create_merge_request_closing_issue(user, project, issue) + merge_merge_requests_closing_issue(user, project, issue) expect(subject[:issue].median).to be_nil end diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index 6fbb2a2d102..d366e2b723a 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -29,8 +29,8 @@ describe 'CycleAnalytics#plan' do context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name]) end]], post_fn: -> (context, data) do - context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name]) - context.merge_merge_requests_closing_issue(data[:issue]) + context.create_merge_request_closing_issue(context.user, context.project, data[:issue], source_branch: data[:branch_name]) + context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) end) context "when a regular label (instead of a list label) is added to the issue" do @@ -41,8 +41,8 @@ describe 'CycleAnalytics#plan' do issue.update(label_ids: [label.id]) create_commit_referencing_issue(issue, branch_name: branch_name) - create_merge_request_closing_issue(issue, source_branch: branch_name) - merge_merge_requests_closing_issue(issue) + create_merge_request_closing_issue(user, project, issue, source_branch: branch_name) + merge_merge_requests_closing_issue(user, project, issue) expect(subject[:issue].median).to be_nil end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index f8681c0a2f9..156eb96cfce 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -13,11 +13,11 @@ describe 'CycleAnalytics#production' do data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } }, start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]], before_end_fn: lambda do |context, data| - context.create_merge_request_closing_issue(data[:issue]) - context.merge_merge_requests_closing_issue(data[:issue]) + context.create_merge_request_closing_issue(context.user, context.project, data[:issue]) + context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) end, end_time_conditions: - [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }], + [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master(context.user, context.project) }], ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master @@ -29,14 +29,14 @@ describe 'CycleAnalytics#production' do branch_name: 'master') context.project.repository.commit(sha) - context.deploy_master + context.deploy_master(context.user, context.project) end]]) context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do merge_request = create(:merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master + deploy_master(user, project) expect(subject[:production].median).to be_nil end @@ -45,9 +45,9 @@ describe 'CycleAnalytics#production' do context "when the deployment happens to a non-production environment" do it "returns nil" do issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) + merge_request = create_merge_request_closing_issue(user, project, issue) MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') + deploy_master(user, project, environment: 'staging') expect(subject[:production].median).to be_nil end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 0ac58695b35..0aedfb49cb5 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -13,11 +13,11 @@ describe 'CycleAnalytics#review' do data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } }, start_time_conditions: [["merge request that closes issue is created", -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) + context.create_merge_request_closing_issue(context.user, context.project, data[:issue]) end]], end_time_conditions: [["merge request that closes issue is merged", -> (context, data) do - context.merge_merge_requests_closing_issue(data[:issue]) + context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) end]], post_fn: nil) diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index b66d5623910..0cbda50c688 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -13,15 +13,15 @@ describe 'CycleAnalytics#staging' do phase: :staging, data_fn: lambda do |context| issue = context.create(:issue, project: context.project) - { issue: issue, merge_request: context.create_merge_request_closing_issue(issue) } + { issue: issue, merge_request: context.create_merge_request_closing_issue(context.user, context.project, issue) } end, start_time_conditions: [["merge request that closes issue is merged", -> (context, data) do - context.merge_merge_requests_closing_issue(data[:issue]) + context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) end]], end_time_conditions: [["merge request that closes issue is deployed to production", -> (context, data) do - context.deploy_master + context.deploy_master(context.user, context.project) end], ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| @@ -34,14 +34,14 @@ describe 'CycleAnalytics#staging' do branch_name: 'master') context.project.repository.commit(sha) - context.deploy_master + context.deploy_master(context.user, context.project) end]]) context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do merge_request = create(:merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master + deploy_master(user, project) expect(subject[:staging].median).to be_nil end @@ -50,9 +50,9 @@ describe 'CycleAnalytics#staging' do context "when the deployment happens to a non-production environment" do it "returns nil" do issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) + merge_request = create_merge_request_closing_issue(user, project, issue) MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') + deploy_master(user, project, environment: 'staging') expect(subject[:staging].median).to be_nil end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index 690c09bc2dc..e58b8fdff58 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -12,26 +12,26 @@ describe 'CycleAnalytics#test' do phase: :test, data_fn: lambda do |context| issue = context.create(:issue, project: context.project) - merge_request = context.create_merge_request_closing_issue(issue) + merge_request = context.create_merge_request_closing_issue(context.user, context.project, issue) pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project, head_pipeline_of: merge_request) { pipeline: pipeline, issue: issue } end, start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]], end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]], post_fn: -> (context, data) do - context.merge_merge_requests_closing_issue(data[:issue]) + context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) end) context "when the pipeline is for a regular merge request (that doesn't close an issue)" do it "returns nil" do issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) + merge_request = create_merge_request_closing_issue(user, project, issue) pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) pipeline.run! pipeline.succeed! - merge_merge_requests_closing_issue(issue) + merge_merge_requests_closing_issue(user, project, issue) expect(subject[:test].median).to be_nil end @@ -51,13 +51,13 @@ describe 'CycleAnalytics#test' do context "when the pipeline is dropped (failed)" do it "returns nil" do issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) + merge_request = create_merge_request_closing_issue(user, project, issue) pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) pipeline.run! pipeline.drop! - merge_merge_requests_closing_issue(issue) + merge_merge_requests_closing_issue(user, project, issue) expect(subject[:test].median).to be_nil end @@ -66,13 +66,13 @@ describe 'CycleAnalytics#test' do context "when the pipeline is cancelled" do it "returns nil" do issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) + merge_request = create_merge_request_closing_issue(user, project, issue) pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) pipeline.run! pipeline.cancel! - merge_merge_requests_closing_issue(issue) + merge_merge_requests_closing_issue(user, project, issue) expect(subject[:test].median).to be_nil end diff --git a/spec/models/cycle_analytics_spec.rb b/spec/models/cycle_analytics_spec.rb new file mode 100644 index 00000000000..0fe24870f02 --- /dev/null +++ b/spec/models/cycle_analytics_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe CycleAnalytics do + let(:project) { create(:project, :repository) } + let(:from_date) { 10.days.ago } + let(:user) { create(:user, :admin) } + let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } + let(:milestone) { create(:milestone, project: project) } + let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") } + let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) } + + subject { described_class.new(project, from: from_date) } + + describe '#all_medians_per_stage' do + before do + allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) + + create_cycle(user, project, issue, mr, milestone, pipeline) + deploy_master(user, project) + end + + it 'returns every median for each stage for a specific project' do + values = described_class::STAGES.each_with_object({}) do |stage_name, hsh| + hsh[stage_name] = subject[stage_name].median.presence + end + + expect(subject.all_medians_per_stage).to eq(values) + end + end +end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 4f16b73ef38..abfc0896a41 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -18,6 +18,7 @@ describe Group do it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_one(:chat_team) } it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') } + it { is_expected.to have_many(:badges).class_name('GroupBadge') } describe '#members & #requesters' do let(:requester) { create(:user) } diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index 04440d890aa..e66109fd98f 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -47,7 +47,7 @@ describe AsanaService do it 'calls Asana service to create a story' do data = create_data_for_commits('Message from commit. related to #123456') - expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}" + expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.full_name} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}" d1 = double('Asana::Task') expect(d1).to receive(:add_comment).with(text: expected_message) diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 23db29cb541..3e2a166cdd6 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -29,7 +29,7 @@ describe HipchatService do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } - let(:project_name) { project.name_with_namespace.gsub(/\s/, '') } + let(:project_name) { project.full_name.gsub(/\s/, '') } let(:token) { 'verySecret' } let(:server_url) { 'https://hipchat.example.com'} let(:push_sample_data) do @@ -303,7 +303,7 @@ describe HipchatService do message = hipchat.__send__(:create_pipeline_message, data) project_url = project.web_url - project_name = project.name_with_namespace.gsub(/\s/, '') + project_name = project.full_name.gsub(/\s/, '') pipeline_attributes = data[:object_attributes] ref = pipeline_attributes[:ref] ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 748c366efca..54ef0be67ff 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -166,7 +166,6 @@ describe JiraService do # Creates comment expect(WebMock).to have_requested(:post, @comment_url) - # Creates Remote Link in JIRA issue fields expect(WebMock).to have_requested(:post, @remote_link_url).with( body: hash_including( @@ -174,7 +173,7 @@ describe JiraService do object: { url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}", title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.", - icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, status: { resolved: true } } ) diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb index 522cf15f3ba..a5bdf9a9337 100644 --- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -31,10 +31,10 @@ describe MattermostSlashCommandsService do url: 'http://trigger.url', icon_url: 'http://icon.url/icon.png', auto_complete: true, - auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}", + auto_complete_desc: "Perform common operations on: #{project.full_name}", auto_complete_hint: '[help]', - description: "Perform common operations on: #{project.name_with_namespace}", - display_name: "GitLab / #{project.name_with_namespace}", + description: "Perform common operations on: #{project.full_name}", + display_name: "GitLab / #{project.full_name}", method: 'P', username: 'GitLab' }.to_json) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f4faec9e52a..92ea8841123 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -80,6 +80,7 @@ describe Project do it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:clusters) } it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } + it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') } it { is_expected.to have_many(:lfs_file_locks) } context 'after initialized' do @@ -3331,4 +3332,36 @@ describe Project do end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError end end + + describe '#badges' do + let(:project_group) { create(:group) } + let(:project) { create(:project, path: 'avatar', namespace: project_group) } + + before do + create_list(:project_badge, 2, project: project) + create(:group_badge, group: project_group) + end + + it 'returns the project and the project group badges' do + create(:group_badge, group: create(:group)) + + expect(Badge.count).to eq 4 + expect(project.badges.count).to eq 3 + end + + if Group.supports_nested_groups? + context 'with nested_groups' do + let(:parent_group) { create(:group) } + + before do + create_list(:group_badge, 2, group: project_group) + project_group.update(parent: parent_group) + end + + it 'returns the project and the project nested groups badges' do + expect(project.badges.count).to eq 5 + end + end + end + end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 1e7671476f1..8b4b5873704 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -14,13 +14,13 @@ describe ProjectWiki do it { is_expected.to delegate_method(:repository_storage_path).to :project } it { is_expected.to delegate_method(:hashed_storage?).to :project } - describe "#path_with_namespace" do + describe "#full_path" do it "returns the project path with namespace with the .wiki extension" do - expect(subject.path_with_namespace).to eq(project.full_path + '.wiki') + expect(subject.full_path).to eq(project.full_path + '.wiki') end it 'returns the same value as #full_path' do - expect(subject.path_with_namespace).to eq(subject.full_path) + expect(subject.full_path).to eq(subject.full_path) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3531de244bd..00b5226d874 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1635,6 +1635,32 @@ describe User do end end + describe '#authorizations_for_projects' do + let!(:user) { create(:user) } + subject { Project.where("EXISTS (?)", user.authorizations_for_projects) } + + it 'includes projects that belong to a user, but no other projects' do + owned = create(:project, :private, namespace: user.namespace) + member = create(:project, :private).tap { |p| p.add_master(user) } + other = create(:project) + + expect(subject).to include(owned) + expect(subject).to include(member) + expect(subject).not_to include(other) + end + + it 'includes projects a user has access to, but no other projects' do + other_user = create(:user) + accessible = create(:project, :private, namespace: other_user.namespace) do |project| + project.add_developer(user) + end + other = create(:project) + + expect(subject).to include(accessible) + expect(subject).not_to include(other) + end + end + describe '#authorized_projects', :delete do context 'with a minimum access level' do it 'includes projects for which the user is an owner' do diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb new file mode 100644 index 00000000000..ae64a9ca162 --- /dev/null +++ b/spec/requests/api/badges_spec.rb @@ -0,0 +1,367 @@ +require 'spec_helper' + +describe API::Badges do + let(:master) { create(:user, username: 'master_user') } + let(:developer) { create(:user) } + let(:access_requester) { create(:user) } + let(:stranger) { create(:user) } + let(:project_group) { create(:group) } + let(:project) { setup_project } + let!(:group) { setup_group } + + shared_context 'source helpers' do + def get_source(source_type) + source_type == 'project' ? project : group + end + end + + shared_examples 'GET /:sources/:id/badges' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges", stranger) } + end + + %i[master developer access_requester stranger].each do |type| + context "when authenticated as a #{type}" do + it 'returns 200' do + user = public_send(type) + badges_count = source_type == 'project' ? 3 : 2 + + get api("/#{source_type.pluralize}/#{source.id}/badges", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(badges_count) + end + end + end + + it 'avoids N+1 queries' do + # Establish baseline + get api("/#{source_type.pluralize}/#{source.id}/badges", master) + + control = ActiveRecord::QueryRecorder.new do + get api("/#{source_type.pluralize}/#{source.id}/badges", master) + end + + project.add_developer(create(:user)) + + expect do + get api("/#{source_type.pluralize}/#{source.id}/badges", master) + end.not_to exceed_query_limit(control) + end + end + end + + shared_examples 'GET /:sources/:id/badges/:badge_id' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges/#{developer.id}", stranger) } + end + + context 'when authenticated as a non-member' do + %i[master developer access_requester stranger].each do |type| + let(:badge) { source.badges.first } + + context "as a #{type}" do + it 'returns 200' do + user = public_send(type) + + get api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(badge.id) + expect(json_response['link_url']).to eq(badge.link_url) + expect(json_response['rendered_link_url']).to eq(badge.rendered_link_url) + expect(json_response['image_url']).to eq(badge.image_url) + expect(json_response['rendered_image_url']).to eq(badge.rendered_image_url) + expect(json_response['kind']).to eq source_type + end + end + end + end + end + end + + shared_examples 'POST /:sources/:id/badges' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + let(:example_url) { 'http://www.example.com' } + let(:example_url2) { 'http://www.example1.com' } + + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + post api("/#{source_type.pluralize}/#{source.id}/badges", stranger), + link_url: example_url, image_url: example_url2 + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + post api("/#{source_type.pluralize}/#{source.id}/badges", user), + link_url: example_url, image_url: example_url2 + + expect(response).to have_gitlab_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'creates a new badge' do + expect do + post api("/#{source_type.pluralize}/#{source.id}/badges", master), + link_url: example_url, image_url: example_url2 + + expect(response).to have_gitlab_http_status(201) + end.to change { source.badges.count }.by(1) + + expect(json_response['link_url']).to eq(example_url) + expect(json_response['image_url']).to eq(example_url2) + expect(json_response['kind']).to eq source_type + end + end + + it 'returns 400 when link_url is not given' do + post api("/#{source_type.pluralize}/#{source.id}/badges", master), + link_url: example_url + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 400 when image_url is not given' do + post api("/#{source_type.pluralize}/#{source.id}/badges", master), + image_url: example_url2 + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 400 when link_url or image_url is not valid' do + post api("/#{source_type.pluralize}/#{source.id}/badges", master), + link_url: 'whatever', image_url: 'whatever' + + expect(response).to have_gitlab_http_status(400) + end + end + end + + shared_examples 'PUT /:sources/:id/badges/:badge_id' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + + context "with :sources == #{source_type.pluralize}" do + let(:badge) { source.badges.first } + let(:example_url) { 'http://www.example.com' } + let(:example_url2) { 'http://www.example1.com' } + + it_behaves_like 'a 404 response when source is private' do + let(:route) do + put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", stranger), + link_url: example_url + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user), + link_url: example_url + + expect(response).to have_gitlab_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'updates the member' do + put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master), + link_url: example_url, image_url: example_url2 + + expect(response).to have_gitlab_http_status(200) + expect(json_response['link_url']).to eq(example_url) + expect(json_response['image_url']).to eq(example_url2) + expect(json_response['kind']).to eq source_type + end + end + + it 'returns 400 when link_url or image_url is not valid' do + put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master), + link_url: 'whatever', image_url: 'whatever' + + expect(response).to have_gitlab_http_status(400) + end + end + end + + shared_examples 'DELETE /:sources/:id/badges/:badge_id' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + + context "with :sources == #{source_type.pluralize}" do + let(:badge) { source.badges.first } + + it_behaves_like 'a 404 response when source is private' do + let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", stranger) } + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester developer stranger].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user) + + expect(response).to have_gitlab_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'deletes the badge' do + expect do + delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master) + + expect(response).to have_gitlab_http_status(204) + end.to change { source.badges.count }.by(-1) + end + + it_behaves_like '412 response' do + let(:request) { api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master) } + end + end + + it 'returns 404 if badge does not exist' do + delete api("/#{source_type.pluralize}/#{source.id}/badges/123", master) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + shared_examples 'GET /:sources/:id/badges/render' do |source_type| + include_context 'source helpers' + + let(:source) { get_source(source_type) } + let(:example_url) { 'http://www.example.com' } + let(:example_url2) { 'http://www.example1.com' } + + context "with :sources == #{source_type.pluralize}" do + it_behaves_like 'a 404 response when source is private' do + let(:route) do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", stranger) + end + end + + context 'when authenticated as a non-member or member with insufficient rights' do + %i[access_requester stranger developer].each do |type| + context "as a #{type}" do + it 'returns 403' do + user = public_send(type) + + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", user) + + expect(response).to have_gitlab_http_status(403) + end + end + end + end + + context 'when authenticated as a master/owner' do + it 'gets the rendered badge values' do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", master) + + expect(response).to have_gitlab_http_status(200) + + expect(json_response.keys).to contain_exactly('link_url', 'rendered_link_url', 'image_url', 'rendered_image_url') + expect(json_response['link_url']).to eq(example_url) + expect(json_response['image_url']).to eq(example_url2) + expect(json_response['rendered_link_url']).to eq(example_url) + expect(json_response['rendered_image_url']).to eq(example_url2) + end + end + + it 'returns 400 when link_url is not given' do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}", master) + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 400 when image_url is not given' do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?image_url=#{example_url}", master) + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 400 when link_url or image_url is not valid' do + get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=whatever&image_url=whatever", master) + + expect(response).to have_gitlab_http_status(400) + end + end + end + + context 'when deleting a badge' do + context 'and the source is a project' do + it 'cannot delete badges owned by the project group' do + delete api("/projects/#{project.id}/badges/#{project_group.badges.first.id}", master) + + expect(response).to have_gitlab_http_status(403) + end + end + end + + describe 'Endpoints' do + %w(project group).each do |source_type| + it_behaves_like 'GET /:sources/:id/badges', source_type + it_behaves_like 'GET /:sources/:id/badges/:badge_id', source_type + it_behaves_like 'GET /:sources/:id/badges/render', source_type + it_behaves_like 'POST /:sources/:id/badges', source_type + it_behaves_like 'PUT /:sources/:id/badges/:badge_id', source_type + it_behaves_like 'DELETE /:sources/:id/badges/:badge_id', source_type + end + end + + def setup_project + create(:project, :public, :access_requestable, creator_id: master.id, namespace: project_group) do |project| + project.add_developer(developer) + project.add_master(master) + project.request_access(access_requester) + project.project_badges << build(:project_badge, project: project) + project.project_badges << build(:project_badge, project: project) + project_group.badges << build(:group_badge, group: group) + end + end + + def setup_group + create(:group, :public, :access_requestable) do |group| + group.add_developer(developer) + group.add_owner(master) + group.request_access(access_requester) + group.badges << build(:group_badge, group: group) + group.badges << build(:group_badge, group: group) + end + end +end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index e433597f58b..64f51d9843d 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -39,6 +39,27 @@ describe API::Branches do end end + context 'when search parameter is passed' do + context 'and branch exists' do + it 'returns correct branches' do + get api(route, user), per_page: 100, search: branch_name + + searched_branch_names = json_response.map { |branch| branch['name'] } + project_branch_names = project.repository.branch_names.grep(/#{branch_name}/) + + expect(searched_branch_names).to match_array(project_branch_names) + end + end + + context 'and branch does not exist' do + it 'returns an empty array' do + get api(route, user), per_page: 100, search: 'no_such_branch_name_entropy_of_jabadabadu' + + expect(json_response).to eq [] + end + end + end + context 'when unauthenticated', 'and project is public' do before do project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index d1569e5d650..6614e8cea43 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -163,6 +163,42 @@ describe API::Issues do expect(first_issue['id']).to eq(issue.id) end + context 'filtering before a specific date' do + let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) } + + it 'returns issues created before a specific date' do + get api('/issues?created_before=2000-01-02T00:00:00.060Z', user) + + expect(json_response.size).to eq(1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues updated before a specific date' do + get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user) + + expect(json_response.size).to eq(1) + expect(first_issue['id']).to eq(issue2.id) + end + end + + context 'filtering after a specific date' do + let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) } + + it 'returns issues created after a specific date' do + get api("/issues?created_after=#{issue2.created_at}", user) + + expect(json_response.size).to eq(1) + expect(first_issue['id']).to eq(issue2.id) + end + + it 'returns issues updated after a specific date' do + get api("/issues?updated_after=#{issue2.updated_at}", user) + + expect(json_response.size).to eq(1) + expect(first_issue['id']).to eq(issue2.id) + end + end + it 'returns an array of labeled issues' do get api("/issues", user), labels: label.title diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 658cedd6b5f..484322752c0 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,6 +9,7 @@ describe API::MergeRequests do let(:non_member) { create(:user) } let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } + let(:pipeline) { create(:ci_empty_pipeline) } let(:milestone1) { create(:milestone, title: '0.9', project: project) } let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } @@ -171,6 +172,42 @@ describe API::MergeRequests do end end + it 'returns merge requests created before a specific date' do + merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', created_at: Date.new(2000, 1, 1)) + + get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user) + + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(merge_request2.id) + end + + it 'returns merge requests created after a specific date' do + merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', created_at: 1.week.from_now) + + get api("/merge_requests?created_after=#{merge_request2.created_at}", user) + + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(merge_request2.id) + end + + it 'returns merge requests updated before a specific date' do + merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', updated_at: Date.new(2000, 1, 1)) + + get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user) + + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(merge_request2.id) + end + + it 'returns merge requests updated after a specific date' do + merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', updated_at: 1.week.from_now) + + get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user) + + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(merge_request2.id) + end + context 'search params' do before do merge_request.update(title: 'Search title', description: 'Search description') @@ -500,6 +537,45 @@ describe API::MergeRequests do expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size) end + context 'merge_request_metrics' do + before do + merge_request.metrics.update!(merged_by: user, + latest_closed_by: user, + latest_closed_at: 1.hour.ago, + merged_at: 2.hours.ago, + pipeline: pipeline, + latest_build_started_at: 3.hours.ago, + latest_build_finished_at: 1.hour.ago, + first_deployed_to_production_at: 3.minutes.ago) + end + + it 'has fields from merge request metrics' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) + + expect(json_response).to include('merged_by', + 'merged_at', + 'closed_by', + 'closed_at', + 'latest_build_started_at', + 'latest_build_finished_at', + 'first_deployed_to_production_at', + 'pipeline') + end + + it 'returns correct values' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.reload.iid}", user) + + expect(json_response['merged_by']['id']).to eq(merge_request.metrics.merged_by_id) + expect(Time.parse json_response['merged_at']).to be_like_time(merge_request.metrics.merged_at) + expect(json_response['closed_by']['id']).to eq(merge_request.metrics.latest_closed_by_id) + expect(Time.parse json_response['closed_at']).to be_like_time(merge_request.metrics.latest_closed_at) + expect(json_response['pipeline']['id']).to eq(merge_request.metrics.pipeline_id) + expect(Time.parse json_response['latest_build_started_at']).to be_like_time(merge_request.metrics.latest_build_started_at) + expect(Time.parse json_response['latest_build_finished_at']).to be_like_time(merge_request.metrics.latest_build_finished_at) + expect(Time.parse json_response['first_deployed_to_production_at']).to be_like_time(merge_request.metrics.first_deployed_to_production_at) + end + end + it "returns a 404 error if merge_request_iid not found" do get api("/projects/#{project.id}/merge_requests/999", user) expect(response).to have_gitlab_http_status(404) diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index 025165622b7..dc3a116c060 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -16,7 +16,7 @@ describe API::PagesDomains do let(:route) { "/projects/#{project.id}/pages/domains" } let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" } - let(:route_domain_path) { "/projects/#{project.path_with_namespace.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" } + let(:route_domain_path) { "/projects/#{project.full_path.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" } let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" } let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" } let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" } diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 72cafac3f90..ce1311ac97c 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -1100,11 +1100,13 @@ describe API::Runner do context 'posts artifacts file and metadata file' do let!(:artifacts) { file_upload } + let!(:artifacts_sha256) { Digest::SHA256.file(artifacts.path).hexdigest } let!(:metadata) { file_upload2 } let(:stored_artifacts_file) { job.reload.artifacts_file.file } let(:stored_metadata_file) { job.reload.artifacts_metadata.file } let(:stored_artifacts_size) { job.reload.artifacts_size } + let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 } before do post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token) @@ -1114,6 +1116,7 @@ describe API::Runner do let(:post_data) do { 'file.path' => artifacts.path, 'file.name' => artifacts.original_filename, + 'file.sha256' => artifacts_sha256, 'metadata.path' => metadata.path, 'metadata.name' => metadata.original_filename } end @@ -1123,6 +1126,7 @@ describe API::Runner do expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) expect(stored_artifacts_size).to eq(72821) + expect(stored_artifacts_sha256).to eq(artifacts_sha256) end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 1b0a5eac9b0..6dbbb1ad7bb 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -506,8 +506,8 @@ describe 'Git HTTP requests' do context 'when LDAP is configured' do before do - allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) - allow_any_instance_of(Gitlab::LDAP::Authentication) + allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true) + allow_any_instance_of(Gitlab::Auth::LDAP::Authentication) .to receive(:login).and_return(nil) end @@ -795,9 +795,9 @@ describe 'Git HTTP requests' do let(:path) { 'doesnt/exist.git' } before do - allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) - allow(Gitlab::LDAP::Authentication).to receive(:login).and_return(nil) - allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user) + allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).and_return(true) + allow_any_instance_of(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil) + allow_any_instance_of(Gitlab::Auth::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user) end it_behaves_like 'pulls require Basic HTTP Authentication' diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 98f70e2101b..eef860821e5 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -15,7 +15,7 @@ describe 'cycle analytics events' do end end - deploy_master + deploy_master(user, project) login_as(user) end @@ -119,7 +119,7 @@ describe 'cycle analytics events' do def create_cycle milestone = create(:milestone, project: project) issue.update(milestone: milestone) - mr = create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") + mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) pipeline.run @@ -127,7 +127,7 @@ describe 'cycle analytics events' do create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, pipeline: pipeline, status: :success, author: user) - merge_merge_requests_closing_issue(issue) + merge_merge_requests_closing_issue(user, project, issue) ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) end diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb index b5a55b4ef6e..852b6af9f7f 100644 --- a/spec/serializers/cluster_application_entity_spec.rb +++ b/spec/serializers/cluster_application_entity_spec.rb @@ -26,5 +26,19 @@ describe ClusterApplicationEntity do expect(subject[:status_reason]).to eq(application.status_reason) end end + + context 'for ingress application' do + let(:application) do + build( + :clusters_applications_ingress, + :installed, + external_ip: '111.222.111.222' + ) + end + + it 'includes external_ip' do + expect(subject[:external_ip]).to eq('111.222.111.222') + end + end end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 9128280eb5a..290eeae828e 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -172,7 +172,7 @@ describe Auth::ContainerRegistryAuthenticationService do end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -200,7 +200,7 @@ describe Auth::ContainerRegistryAuthenticationService do end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -239,7 +239,7 @@ describe Auth::ContainerRegistryAuthenticationService do end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -270,7 +270,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow anyone to delete images' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -311,7 +311,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow anyone to delete images' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -323,7 +323,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow anyone to pull or push images' do let(:current_user) { create(:user, external: true) } let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull,push" } + { scope: "repository:#{project.full_path}:pull,push" } end it_behaves_like 'an inaccessible' @@ -333,7 +333,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow anyone to delete images' do let(:current_user) { create(:user, external: true) } let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:*" } + { scope: "repository:#{project.full_path}:*" } end it_behaves_like 'an inaccessible' @@ -359,7 +359,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'allow to delete images' do let(:current_params) do - { scope: "repository:#{current_project.path_with_namespace}:*" } + { scope: "repository:#{current_project.full_path}:*" } end it_behaves_like 'a deletable' do @@ -398,7 +398,7 @@ describe Auth::ContainerRegistryAuthenticationService do context 'disallow to delete images' do let(:current_params) do - { scope: "repository:#{current_project.path_with_namespace}:*" } + { scope: "repository:#{current_project.full_path}:*" } end it_behaves_like 'an inaccessible' do diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb new file mode 100644 index 00000000000..bf038595a4d --- /dev/null +++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe Clusters::Applications::CheckIngressIpAddressService do + let(:application) { create(:clusters_applications_ingress, :installed) } + let(:service) { described_class.new(application) } + let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) } + let(:ingress) { [{ ip: '111.222.111.222' }] } + let(:exclusive_lease) { instance_double(Gitlab::ExclusiveLease, try_obtain: true) } + + let(:kube_service) do + ::Kubeclient::Resource.new( + { + status: { + loadBalancer: { + ingress: ingress + } + } + } + ) + end + + subject { service.execute } + + before do + allow(application.cluster).to receive(:kubeclient).and_return(kubeclient) + allow(Gitlab::ExclusiveLease) + .to receive(:new) + .with("check_ingress_ip_address_service:#{application.id}", timeout: 15.seconds.to_i) + .and_return(exclusive_lease) + end + + describe '#execute' do + context 'when the ingress ip address is available' do + it 'updates the external_ip for the app' do + subject + + expect(application.external_ip).to eq('111.222.111.222') + end + end + + context 'when the ingress ip address is not available' do + let(:ingress) { nil } + + it 'does not error' do + subject + end + end + + context 'when the exclusive lease cannot be obtained' do + before do + allow(exclusive_lease) + .to receive(:try_obtain) + .and_return(false) + end + + it 'does not call kubeclient' do + subject + + expect(kubeclient).not_to have_received(:get_service) + end + end + + context 'when there is already an external_ip' do + let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '001.111.002.111') } + + it 'does not call kubeclient' do + subject + + expect(kubeclient).not_to have_received(:get_service) + end + end + end +end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 3a935d98540..6aed481939e 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -15,8 +15,8 @@ describe MergeRequests::BuildService do let(:target_branch) { 'master' } let(:merge_request) { service.execute } let(:compare) { double(:compare, commits: commits) } - let(:commit_1) { double(:commit_1, safe_message: "Initial commit\n\nCreate the app") } - let(:commit_2) { double(:commit_2, safe_message: 'This is a bad commit message!') } + let(:commit_1) { double(:commit_1, sha: 'f00ba7', safe_message: "Initial commit\n\nCreate the app") } + let(:commit_2) { double(:commit_2, sha: 'f00ba7', safe_message: 'This is a bad commit message!') } let(:commits) { nil } let(:service) do diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 5d226f34d2d..44a83c436cb 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -28,6 +28,7 @@ describe MergeRequests::CreateService do it 'creates an MR' do expect(merge_request).to be_valid + expect(merge_request.work_in_progress?).to be(false) expect(merge_request.title).to eq('Awesome merge_request') expect(merge_request.assignee).to be_nil expect(merge_request.merge_params['force_remove_source_branch']).to eq('1') @@ -62,6 +63,40 @@ describe MergeRequests::CreateService do expect(Event.where(attributes).count).to eq(1) end + describe 'when marked with /wip' do + context 'in title and in description' do + let(:opts) do + { + title: 'WIP: Awesome merge_request', + description: "well this is not done yet\n/wip", + source_branch: 'feature', + target_branch: 'master', + assignee: assignee + } + end + + it 'sets MR to WIP' do + expect(merge_request.work_in_progress?).to be(true) + end + end + + context 'in description only' do + let(:opts) do + { + title: 'Awesome merge_request', + description: "well this is not done yet\n/wip", + source_branch: 'feature', + target_branch: 'master', + assignee: assignee + } + end + + it 'sets MR to WIP' do + expect(merge_request.work_in_progress?).to be(true) + end + end + end + context 'when merge request is assigned to someone' do let(:opts) do { diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 0ae26e87154..f5cff66de6d 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -57,32 +57,55 @@ describe Notes::CreateService do end end - describe 'note with commands' do - describe '/close, /label, /assign & /milestone' do - let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) } + context 'note with commands' do + context 'as a user who can update the target' do + context '/close, /label, /assign & /milestone' do + let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) } - it 'saves the note and does not alter the note text' do - expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original + it 'saves the note and does not alter the note text' do + expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original - note = described_class.new(project, user, opts.merge(note: note_text)).execute + note = described_class.new(project, user, opts.merge(note: note_text)).execute - expect(note.note).to eq "HELLO\nWORLD" + expect(note.note).to eq "HELLO\nWORLD" + end + end + + context '/merge with sha option' do + let(:note_text) { %(HELLO\n/merge\nWORLD) } + let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') } + + it 'saves the note and exectues merge command' do + note = described_class.new(project, user, params).execute + + expect(note.note).to eq "HELLO\nWORLD" + end end end - describe '/merge with sha option' do - let(:note_text) { %(HELLO\n/merge\nWORLD) } - let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') } + context 'as a user who cannot update the target' do + let(:note_text) { "HELLO\n/todo\n/assign #{user.to_reference}\nWORLD" } + let(:note) { described_class.new(project, user, opts.merge(note: note_text)).execute } - it 'saves the note and exectues merge command' do - note = described_class.new(project, user, params).execute + before do + project.team.find_member(user.id).update!(access_level: Gitlab::Access::GUEST) + end + + it 'applies commands the user can execute' do + expect { note }.to change { user.todos_pending_count }.from(0).to(1) + end + + it 'does not apply commands the user cannot execute' do + expect { note }.not_to change { issue.assignees } + end + it 'saves the note' do expect(note.note).to eq "HELLO\nWORLD" end end end - describe 'personal snippet note' do + context 'personal snippet note' do subject { described_class.new(nil, user, params).execute } let(:snippet) { create(:personal_snippet) } @@ -103,7 +126,7 @@ describe Notes::CreateService do end end - describe 'note with emoji only' do + context 'note with emoji only' do it 'creates regular note' do opts = { note: ':smile: ', diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index 5eafe56c99d..b1e218821d2 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -165,31 +165,17 @@ describe Notes::QuickActionsService do let(:note) { create(:note_on_issue, project: project) } - context 'with no current_user' do - it 'returns false' do - expect(described_class.supported?(note, nil)).to be_falsy - end - end - - context 'when current_user cannot update the noteable' do - it 'returns false' do - user = create(:user) - - expect(described_class.supported?(note, user)).to be_falsy - end - end - - context 'when current_user can update the noteable' do + context 'with a note on an issue' do it 'returns true' do - expect(described_class.supported?(note, master)).to be_truthy + expect(described_class.supported?(note)).to be_truthy end + end - context 'with a note on a commit' do - let(:note) { create(:note_on_commit, project: project) } + context 'with a note on a commit' do + let(:note) { create(:note_on_commit, project: project) } - it 'returns false' do - expect(described_class.supported?(note, nil)).to be_falsy - end + it 'returns false' do + expect(described_class.supported?(note)).to be_falsy end end end @@ -201,7 +187,7 @@ describe Notes::QuickActionsService do service = described_class.new(project, master) note = create(:note_on_issue, project: project) - expect(described_class).to receive(:supported?).with(note, master) + expect(described_class).to receive(:supported?).with(note) service.supported?(note) end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index c40cd5b7548..51396d34f8f 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -30,6 +30,7 @@ describe SystemHooksService do :old_path_with_namespace ) end + it do project.old_path_with_namespace = 'transfered_from_path' expect(event_data(project, :transfer)).to include( @@ -45,18 +46,21 @@ describe SystemHooksService do :owner_name, :owner_email ) end + it do expect(event_data(group, :destroy)).to include( :event_name, :name, :created_at, :updated_at, :path, :group_id, :owner_name, :owner_email ) end + it do expect(event_data(group_member, :create)).to include( :event_name, :created_at, :updated_at, :group_name, :group_path, :group_id, :user_id, :user_username, :user_name, :user_email, :group_access ) end + it do expect(event_data(group_member, :destroy)).to include( :event_name, :created_at, :updated_at, :group_name, :group_path, @@ -70,6 +74,14 @@ describe SystemHooksService do expect(data[:project_visibility]).to eq('private') end + it 'handles nil datetime columns' do + user.update_attributes(created_at: nil, updated_at: nil) + data = event_data(user, :destroy) + + expect(data[:created_at]).to be(nil) + expect(data[:updated_at]).to be(nil) + end + context 'group_rename' do it 'contains old and new path' do allow(group).to receive(:path_was).and_return('old-path') diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5b5edc1aa0d..a3893188c6e 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -789,7 +789,7 @@ describe SystemNoteService do object: { url: project_commit_url(project, commit), title: "GitLab: Mentioned on commit - #{commit.title}", - icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, status: { resolved: false } } ) @@ -815,7 +815,7 @@ describe SystemNoteService do object: { url: project_issue_url(project, issue), title: "GitLab: Mentioned on issue - #{issue.title}", - icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, status: { resolved: false } } ) @@ -841,7 +841,7 @@ describe SystemNoteService do object: { url: project_snippet_url(project, snippet), title: "GitLab: Mentioned on snippet - #{snippet.title}", - icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, status: { resolved: false } } ) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c0f3366fb52..9f6f0204a16 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -186,6 +186,10 @@ RSpec.configure do |config| example.run if Gitlab::Database.postgresql? end + config.around(:each, :mysql) do |example| + example.run if Gitlab::Database.mysql? + end + # This makes sure the `ApplicationController#can?` method is stubbed with the # original implementation for all view specs. config.before(:each, type: :view) do diff --git a/spec/support/bare_repo_operations.rb b/spec/support/bare_repo_operations.rb index 38d11992dc2..8eeaa37d3c5 100644 --- a/spec/support/bare_repo_operations.rb +++ b/spec/support/bare_repo_operations.rb @@ -11,6 +11,14 @@ class BareRepoOperations @path_to_repo = path_to_repo end + def commit_tree(tree_id, msg, parent: EMPTY_TREE_ID) + commit_tree_args = ['commit-tree', tree_id, '-m', msg] + commit_tree_args += ['-p', parent] unless parent == EMPTY_TREE_ID + commit_id = execute(commit_tree_args) + + commit_id[0] + end + # Based on https://stackoverflow.com/a/25556917/1856239 def commit_file(file, dst_path, branch = 'master') head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || EMPTY_TREE_ID @@ -26,11 +34,9 @@ class BareRepoOperations tree_id = execute(['write-tree']) - commit_tree_args = ['commit-tree', tree_id[0], '-m', "Add #{dst_path}"] - commit_tree_args += ['-p', head_id] unless head_id == EMPTY_TREE_ID - commit_id = execute(commit_tree_args) + commit_id = commit_tree(tree_id[0], "Add #{dst_path}", parent: head_id) - execute(['update-ref', "refs/heads/#{branch}", commit_id[0]]) + execute(['update-ref', "refs/heads/#{branch}", commit_id]) end private diff --git a/spec/support/cluster_application_spec.rb b/spec/support/cluster_application_spec.rb deleted file mode 100644 index ab77910a050..00000000000 --- a/spec/support/cluster_application_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -shared_examples 'cluster application specs' do - let(:factory_name) { described_class.to_s.downcase.gsub("::", "_") } - - describe '#name' do - it 'is .application_name' do - expect(subject.name).to eq(described_class.application_name) - end - - it 'is recorded in Clusters::Cluster::APPLICATIONS' do - expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class) - end - end - - describe '#status' do - let(:cluster) { create(:cluster, :provided_by_gcp) } - - subject { described_class.new(cluster: cluster) } - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - - context 'when application helm is scheduled' do - before do - create(factory_name, :scheduled, cluster: cluster) - end - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - end - - context 'when application helm is installed' do - before do - create(:clusters_applications_helm, :installed, cluster: cluster) - end - - it 'defaults to :installable' do - expect(subject.status_name).to be(:installable) - end - end - end - - describe '#install_command' do - it 'has all the needed information' do - expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false) - end - end - - describe 'status state machine' do - describe '#make_installing' do - subject { create(factory_name, :scheduled) } - - it 'is installing' do - subject.make_installing! - - expect(subject).to be_installing - end - end - - describe '#make_installed' do - subject { create(factory_name, :installing) } - - it 'is installed' do - subject.make_installed - - expect(subject).to be_installed - end - end - - describe '#make_errored' do - subject { create(factory_name, :installing) } - let(:reason) { 'some errors' } - - it 'is errored' do - subject.make_errored(reason) - - expect(subject).to be_errored - expect(subject.status_reason).to eq(reason) - end - end - - describe '#make_scheduled' do - subject { create(factory_name, :installable) } - - it 'is scheduled' do - subject.make_scheduled - - expect(subject).to be_scheduled - end - - describe 'when was errored' do - subject { create(factory_name, :errored) } - - it 'clears #status_reason' do - expect(subject.status_reason).not_to be_nil - - subject.make_scheduled! - - expect(subject.status_reason).to be_nil - end - end - end - end -end diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index d5ef80cfab2..73cc64c0b74 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -26,7 +26,19 @@ module CycleAnalyticsHelpers ref: 'refs/heads/master').execute end - def create_merge_request_closing_issue(issue, message: nil, source_branch: nil, commit_message: 'commit message') + def create_cycle(user, project, issue, mr, milestone, pipeline) + issue.update(milestone: milestone) + pipeline.run + + ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user) + + merge_merge_requests_closing_issue(user, project, issue) + ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) + + ci_build + end + + def create_merge_request_closing_issue(user, project, issue, message: nil, source_branch: nil, commit_message: 'commit message') if !source_branch || project.repository.commit(source_branch).blank? source_branch = generate(:branch) project.repository.add_branch(user, source_branch, 'master') @@ -52,19 +64,19 @@ module CycleAnalyticsHelpers mr end - def merge_merge_requests_closing_issue(issue) + def merge_merge_requests_closing_issue(user, project, issue) merge_requests = issue.closed_by_merge_requests(user) merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) } end - def deploy_master(environment: 'production') + def deploy_master(user, project, environment: 'production') dummy_job = case environment when 'production' - dummy_production_job + dummy_production_job(user, project) when 'staging' - dummy_staging_job + dummy_staging_job(user, project) else raise ArgumentError end @@ -72,25 +84,24 @@ module CycleAnalyticsHelpers CreateDeploymentService.new(dummy_job).execute end - def dummy_production_job - @dummy_job ||= new_dummy_job('production') + def dummy_production_job(user, project) + new_dummy_job(user, project, 'production') end - def dummy_staging_job - @dummy_job ||= new_dummy_job('staging') + def dummy_staging_job(user, project) + new_dummy_job(user, project, 'staging') end - def dummy_pipeline - @dummy_pipeline ||= - Ci::Pipeline.new( - sha: project.repository.commit('master').sha, - ref: 'master', - source: :push, - project: project, - protected: false) + def dummy_pipeline(project) + Ci::Pipeline.new( + sha: project.repository.commit('master').sha, + ref: 'master', + source: :push, + project: project, + protected: false) end - def new_dummy_job(environment) + def new_dummy_job(user, project, environment) project.environments.find_or_create_by(name: environment) Ci::Build.new( @@ -101,7 +112,7 @@ module CycleAnalyticsHelpers tag: false, name: 'dummy', stage: 'dummy', - pipeline: dummy_pipeline, + pipeline: dummy_pipeline(project), protected: false) end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 2c20821ac3f..f61469f673d 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -127,7 +127,6 @@ shared_examples 'issuable record that supports quick actions in its description it "does not close the #{issuable_type}" do write_note("/close") - expect(page).to have_content '/close' expect(page).not_to have_content 'Commands applied' expect(issuable).to be_open @@ -165,7 +164,6 @@ shared_examples 'issuable record that supports quick actions in its description it "does not reopen the #{issuable_type}" do write_note("/reopen") - expect(page).to have_content '/reopen' expect(page).not_to have_content 'Commands applied' expect(issuable).to be_closed @@ -195,10 +193,9 @@ shared_examples 'issuable record that supports quick actions in its description visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end - it "does not reopen the #{issuable_type}" do + it "does not change the #{issuable_type} title" do write_note("/title Awesome new title") - expect(page).to have_content '/title' expect(page).not_to have_content 'Commands applied' expect(issuable.reload.title).not_to eq 'Awesome new title' diff --git a/spec/support/gitlab_verify.rb b/spec/support/gitlab_verify.rb new file mode 100644 index 00000000000..13e2e37624d --- /dev/null +++ b/spec/support/gitlab_verify.rb @@ -0,0 +1,45 @@ +RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do + describe 'batching' do + let(:first_batch) { objects[0].id..objects[0].id } + let(:second_batch) { objects[1].id..objects[1].id } + let(:third_batch) { objects[2].id..objects[2].id } + + it 'iterates through objects in batches' do + expect(collect_ranges).to eq([first_batch, second_batch, third_batch]) + end + + it 'allows the starting ID to be specified' do + expect(collect_ranges(start: second_batch.first)).to eq([second_batch, third_batch]) + end + + it 'allows the finishing ID to be specified' do + expect(collect_ranges(finish: second_batch.last)).to eq([first_batch, second_batch]) + end + end +end + +module GitlabVerifyHelpers + def collect_ranges(args = {}) + verifier = described_class.new(args.merge(batch_size: 1)) + + collect_results(verifier).map { |range, _| range } + end + + def collect_failures + verifier = described_class.new(batch_size: 1) + + out = {} + + collect_results(verifier).map { |_, failures| out.merge!(failures) } + + out + end + + def collect_results(verifier) + out = [] + + verifier.run_batches { |*args| out << args } + + out + end +end diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb index 28d39a32f02..081ce0ad7b7 100644 --- a/spec/support/ldap_helpers.rb +++ b/spec/support/ldap_helpers.rb @@ -1,13 +1,13 @@ module LdapHelpers def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap)) - ::Gitlab::LDAP::Adapter.new(provider, ldap) + ::Gitlab::Auth::LDAP::Adapter.new(provider, ldap) end def user_dn(uid) "uid=#{uid},ou=users,dc=example,dc=com" end - # Accepts a hash of Gitlab::LDAP::Config keys and values. + # Accepts a hash of Gitlab::Auth::LDAP::Config keys and values. # # Example: # stub_ldap_config( @@ -15,21 +15,21 @@ module LdapHelpers # admin_group: 'my-admin-group' # ) def stub_ldap_config(messages) - allow_any_instance_of(::Gitlab::LDAP::Config).to receive_messages(messages) + allow_any_instance_of(::Gitlab::Auth::LDAP::Config).to receive_messages(messages) end # Stub an LDAP person search and provide the return entry. Specify `nil` for # `entry` to simulate when an LDAP person is not found # # Example: - # adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap)) + # adapter = ::Gitlab::Auth::LDAP::Adapter.new('ldapmain', double(:ldap)) # ldap_user_entry = ldap_user_entry('john_doe') # # stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter) def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain') - return_value = ::Gitlab::LDAP::Person.new(entry, provider) if entry.present? + return_value = ::Gitlab::Auth::LDAP::Person.new(entry, provider) if entry.present? - allow(::Gitlab::LDAP::Person) + allow(::Gitlab::Auth::LDAP::Person) .to receive(:find_by_uid).with(uid, any_args).and_return(return_value) end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index b52b6a28c54..d08183846a0 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -138,7 +138,7 @@ module LoginHelpers Rails.application.routes.draw do post '/users/auth/saml' => 'omniauth_callbacks#saml' end - allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config) + allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config) stub_omniauth_setting(messages) allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml') allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') @@ -149,10 +149,10 @@ module LoginHelpers end def stub_basic_saml_config - allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } }) + allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } }) end def stub_saml_group_config(groups) - allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } }) + allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } }) end end diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb new file mode 100644 index 00000000000..87d12a784ba --- /dev/null +++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb @@ -0,0 +1,70 @@ +shared_examples 'cluster application core specs' do |application_name| + it { is_expected.to belong_to(:cluster) } + it { is_expected.to validate_presence_of(:cluster) } + + describe '#name' do + it 'is .application_name' do + expect(subject.name).to eq(described_class.application_name) + end + + it 'is recorded in Clusters::Cluster::APPLICATIONS' do + expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class) + end + end + + describe 'status state machine' do + describe '#make_installing' do + subject { create(application_name, :scheduled) } + + it 'is installing' do + subject.make_installing! + + expect(subject).to be_installing + end + end + + describe '#make_installed' do + subject { create(application_name, :installing) } + + it 'is installed' do + subject.make_installed + + expect(subject).to be_installed + end + end + + describe '#make_errored' do + subject { create(application_name, :installing) } + let(:reason) { 'some errors' } + + it 'is errored' do + subject.make_errored(reason) + + expect(subject).to be_errored + expect(subject.status_reason).to eq(reason) + end + end + + describe '#make_scheduled' do + subject { create(application_name, :installable) } + + it 'is scheduled' do + subject.make_scheduled + + expect(subject).to be_scheduled + end + + describe 'when was errored' do + subject { create(application_name, :errored) } + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_scheduled! + + expect(subject.status_reason).to be_nil + end + end + end + end +end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb new file mode 100644 index 00000000000..765dd32f4ba --- /dev/null +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -0,0 +1,31 @@ +shared_examples 'cluster application status specs' do |application_name| + describe '#status' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + subject { described_class.new(cluster: cluster) } + + it 'sets a default status' do + expect(subject.status_name).to be(:not_installable) + end + + context 'when application helm is scheduled' do + before do + create(:clusters_applications_helm, :scheduled, cluster: cluster) + end + + it 'defaults to :not_installable' do + expect(subject.status_name).to be(:not_installable) + end + end + + context 'when application is scheduled' do + before do + create(:clusters_applications_helm, :installed, cluster: cluster) + end + + it 'sets a default status' do + expect(subject.status_name).to be(:installable) + end + end + end +end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index e827a8da0b7..5e1ce19eafb 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -337,6 +337,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do before do chat_service.notify_only_default_branch = true + WebMock.stub_request(:post, webhook_url) end it 'does not call the Slack/Mattermost API for pipeline events' do @@ -345,6 +346,23 @@ RSpec.shared_examples 'slack or mattermost notifications' do expect(result).to be_falsy end + + it 'does not notify push events if they are not for the default branch' do + ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" + push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + + chat_service.execute(push_sample_data) + + expect(WebMock).not_to have_requested(:post, webhook_url) + end + + it 'notifies about push events for the default branch' do + push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) + + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end end context 'when disabled' do diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb index 538ff952bf4..4eda618b6d6 100644 --- a/spec/tasks/gitlab/check_rake_spec.rb +++ b/spec/tasks/gitlab/check_rake_spec.rb @@ -11,8 +11,8 @@ describe 'gitlab:ldap:check rake task' do context 'when LDAP is not enabled' do it 'does not attempt to bind or search for users' do - expect(Gitlab::LDAP::Config).not_to receive(:providers) - expect(Gitlab::LDAP::Adapter).not_to receive(:open) + expect(Gitlab::Auth::LDAP::Config).not_to receive(:providers) + expect(Gitlab::Auth::LDAP::Adapter).not_to receive(:open) run_rake_task('gitlab:ldap:check') end @@ -23,12 +23,12 @@ describe 'gitlab:ldap:check rake task' do let(:adapter) { ldap_adapter('ldapmain', ldap) } before do - allow(Gitlab::LDAP::Config) + allow(Gitlab::Auth::LDAP::Config) .to receive_messages( enabled?: true, providers: ['ldapmain'] ) - allow(Gitlab::LDAP::Adapter).to receive(:open).and_yield(adapter) + allow(Gitlab::Auth::LDAP::Adapter).to receive(:open).and_yield(adapter) allow(adapter).to receive(:users).and_return([]) end diff --git a/spec/tasks/gitlab/lfs/check_rake_spec.rb b/spec/tasks/gitlab/lfs/check_rake_spec.rb new file mode 100644 index 00000000000..2610edf8bac --- /dev/null +++ b/spec/tasks/gitlab/lfs/check_rake_spec.rb @@ -0,0 +1,28 @@ +require 'rake_helper' + +describe 'gitlab:lfs rake tasks' do + describe 'check' do + let!(:lfs_object) { create(:lfs_object, :with_file, :correct_oid) } + + before do + Rake.application.rake_require('tasks/gitlab/lfs/check') + stub_env('VERBOSE' => 'true') + end + + it 'outputs the integrity check for each batch' do + expect { run_rake_task('gitlab:lfs:check') }.to output(/Failures: 0/).to_stdout + end + + it 'errors out about missing files on the file system' do + FileUtils.rm_f(lfs_object.file.path) + + expect { run_rake_task('gitlab:lfs:check') }.to output(/No such file.*#{Regexp.quote(lfs_object.file.path)}/).to_stdout + end + + it 'errors out about invalid checksum' do + File.truncate(lfs_object.file.path, 0) + + expect { run_rake_task('gitlab:lfs:check') }.to output(/Checksum mismatch/).to_stdout + end + end +end diff --git a/spec/tasks/gitlab/uploads_rake_spec.rb b/spec/tasks/gitlab/uploads/check_rake_spec.rb index ac0005e51e0..5d597c66133 100644 --- a/spec/tasks/gitlab/uploads_rake_spec.rb +++ b/spec/tasks/gitlab/uploads/check_rake_spec.rb @@ -5,23 +5,24 @@ describe 'gitlab:uploads rake tasks' do let!(:upload) { create(:upload, path: Rails.root.join('spec/fixtures/banana_sample.gif')) } before do - Rake.application.rake_require 'tasks/gitlab/uploads' + Rake.application.rake_require('tasks/gitlab/uploads/check') + stub_env('VERBOSE' => 'true') end - it 'outputs the integrity check for each uploaded file' do - expect { run_rake_task('gitlab:uploads:check') }.to output(/Checking file \(#{upload.id}\): #{Regexp.quote(upload.absolute_path)}/).to_stdout + it 'outputs the integrity check for each batch' do + expect { run_rake_task('gitlab:uploads:check') }.to output(/Failures: 0/).to_stdout end it 'errors out about missing files on the file system' do - create(:upload) + missing_upload = create(:upload) - expect { run_rake_task('gitlab:uploads:check') }.to output(/File does not exist on the file system/).to_stdout + expect { run_rake_task('gitlab:uploads:check') }.to output(/No such file.*#{Regexp.quote(missing_upload.absolute_path)}/).to_stdout end it 'errors out about invalid checksum' do upload.update_column(:checksum, '01a3156db2cf4f67ec823680b40b7302f89ab39179124ad219f94919b8a1769e') - expect { run_rake_task('gitlab:uploads:check') }.to output(/File checksum \(9e697aa09fe196909813ee36103e34f721fe47a5fdc8aac0e4e4ac47b9b38282\) does not match the one in the database \(#{upload.checksum}\)/).to_stdout + expect { run_rake_task('gitlab:uploads:check') }.to output(/Checksum mismatch/).to_stdout end end end diff --git a/spec/validators/url_placeholder_validator_spec.rb b/spec/validators/url_placeholder_validator_spec.rb new file mode 100644 index 00000000000..b76d8acdf88 --- /dev/null +++ b/spec/validators/url_placeholder_validator_spec.rb @@ -0,0 +1,39 @@ +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 new file mode 100644 index 00000000000..763dff181d2 --- /dev/null +++ b/spec/validators/url_validator_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe UrlValidator do + let(:validator) { described_class.new(attributes: [:link_url], **options) } + let!(:badge) { build(:badge) } + + 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 = 'http://www.google.es/%{whatever}' + + 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 + badge.link_url = 'http://www.example.com' + + 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/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb index 62af946dcab..15fce65979b 100644 --- a/spec/views/projects/_home_panel.html.haml_spec.rb +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe 'projects/_home_panel' do - let(:project) { create(:project, :public) } + let(:group) { create(:group) } + let(:project) { create(:project, :public, namespace: group) } let(:notification_settings) do user&.notification_settings_for(project) @@ -35,4 +36,55 @@ describe 'projects/_home_panel' do expect(rendered).not_to have_selector('.notification_dropdown') end end + + context 'when project' do + let!(:user) { create(:user) } + let(:badges) { project.badges } + + context 'has no badges' do + it 'should not render any badge' do + render + + expect(rendered).to have_selector('.project-badges') + expect(rendered).not_to have_selector('.project-badges > a') + end + end + + shared_examples 'show badges' do + it 'should render the all badges' do + render + + expect(rendered).to have_selector('.project-badges a') + + badges.each do |badge| + expect(rendered).to have_link(href: badge.rendered_link_url) + end + end + end + + context 'only has group badges' do + before do + create(:group_badge, group: project.group) + end + + it_behaves_like 'show badges' + end + + context 'only has project badges' do + before do + create(:project_badge, project: project) + end + + it_behaves_like 'show badges' + end + + context 'has both group and project badges' do + before do + create(:project_badge, project: project) + create(:group_badge, group: project.group) + end + + it_behaves_like 'show badges' + end + end end diff --git a/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb new file mode 100644 index 00000000000..2e2e9afd25a --- /dev/null +++ b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe ClusterWaitForIngressIpAddressWorker do + describe '#perform' do + let(:service) { instance_double(Clusters::Applications::CheckIngressIpAddressService, execute: true) } + let(:application) { instance_double(Clusters::Applications::Ingress) } + let(:worker) { described_class.new } + + before do + allow(worker) + .to receive(:find_application) + .with('ingress', 117) + .and_yield(application) + + allow(Clusters::Applications::CheckIngressIpAddressService) + .to receive(:new) + .with(application) + .and_return(service) + + allow(described_class) + .to receive(:perform_in) + end + + it 'finds the application and calls CheckIngressIpAddressService#execute' do + worker.perform('ingress', 117) + + expect(service).to have_received(:execute) + end + end +end diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb index 68cfe9d5545..615462380e0 100644 --- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb @@ -25,7 +25,7 @@ describe Gitlab::GithubImport::ObjectImporter do importer_class = double(:importer_class) importer_instance = double(:importer_instance) representation = double(:representation) - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) expect(worker) diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 47297de738b..74539a7e493 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -195,6 +195,12 @@ describe GitGarbageCollectWorker do expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled) end + + it 'cleans up repository after finishing' do + expect_any_instance_of(Project).to receive(:cleanup).and_call_original + + subject.perform(project.id, 'gc', lease_key, lease_uuid) + end end context 'with bitmaps enabled' do diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb index 7c8c665a9b3..48e7eaf32fc 100644 --- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportDiffNoteWorker do describe '#import' do it 'imports a diff note' do - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb index 4116380ff4d..8cf6ac15919 100644 --- a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportIssueWorker do describe '#import' do it 'imports an issue' do - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb index 0ca825a722b..677697c02df 100644 --- a/spec/workers/gitlab/github_import/import_note_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportNoteWorker do describe '#import' do it 'imports a note' do - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb index d49f560af42..e287ddbe0d7 100644 --- a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb +++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportPullRequestWorker do describe '#import' do it 'imports a pull request' do - project = double(:project, path_with_namespace: 'foo/bar') + project = double(:project, full_path: 'foo/bar') client = double(:client) importer = double(:importer) hash = { diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 76ef57b6b1e..ac79d9c0ac1 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -20,32 +20,6 @@ describe ProcessCommitWorker do worker.perform(project.id, -1, commit.to_hash) end - context 'when commit is a merge request merge commit' do - let(:merge_request) do - create(:merge_request, - description: "Closes #{issue.to_reference}", - source_branch: 'feature-merged', - target_branch: 'master', - source_project: project) - end - - let(:commit) do - project.repository.create_branch('feature-merged', 'feature') - - sha = project.repository.merge(user, - merge_request.diff_head_sha, - merge_request, - "Closes #{issue.to_reference}") - project.repository.commit(sha) - end - - it 'it does not close any issues from the commit message' do - expect(worker).not_to receive(:close_issues) - - worker.perform(project.id, user.id, commit.to_hash) - end - end - it 'processes the commit message' do expect(worker).to receive(:process_commit_message).and_call_original @@ -73,13 +47,21 @@ describe ProcessCommitWorker do describe '#process_commit_message' do context 'when pushing to the default branch' do - it 'closes issues that should be closed per the commit message' do + before do allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}") + end + it 'closes issues that should be closed per the commit message' do expect(worker).to receive(:close_issues).with(project, user, user, commit, [issue]) worker.process_commit_message(project, commit, user, user, true) end + + it 'creates cross references' do + expect(commit).to receive(:create_cross_references!).with(user, [issue]) + + worker.process_commit_message(project, commit, user, user, true) + end end context 'when pushing to a non-default branch' do @@ -90,12 +72,44 @@ describe ProcessCommitWorker do worker.process_commit_message(project, commit, user, user, false) end + + it 'does not create cross references' do + expect(commit).to receive(:create_cross_references!).with(user, []) + + worker.process_commit_message(project, commit, user, user, false) + end end - it 'creates cross references' do - expect(commit).to receive(:create_cross_references!) + context 'when commit is a merge request merge commit to the default branch' do + let(:merge_request) do + create(:merge_request, + description: "Closes #{issue.to_reference}", + source_branch: 'feature-merged', + target_branch: 'master', + source_project: project) + end - worker.process_commit_message(project, commit, user, user) + let(:commit) do + project.repository.create_branch('feature-merged', 'feature') + + MergeRequests::MergeService + .new(project, merge_request.author) + .execute(merge_request) + + merge_request.reload.merge_commit + end + + it 'does not close any issues from the commit message' do + expect(worker).not_to receive(:close_issues) + + worker.process_commit_message(project, commit, user, user, true) + end + + it 'still creates cross references' do + expect(commit).to receive(:create_cross_references!).with(user, []) + + worker.process_commit_message(project, commit, user, user, true) + end end end |