diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2017-09-06 21:03:07 +0200 |
---|---|---|
committer | Kamil Trzcinski <ayufan@ayufan.eu> | 2017-09-06 21:03:07 +0200 |
commit | 64db7077740385eabc77178c8fd386330b57183b (patch) | |
tree | 55c2b05f1fa3d20c9702ad5bda7c2f2fe21f4173 /spec | |
parent | 051623d215d6a00983b5c247a7c83f474387622d (diff) | |
parent | cd8ea329f0d64c04e5dee00fb8916268dae7a6f7 (diff) | |
download | gitlab-ce-64db7077740385eabc77178c8fd386330b57183b.tar.gz |
Merge branch 'zj/gitlab-ce-zj-auto-devops-table' into 37158-autodevops-banner
Diffstat (limited to 'spec')
152 files changed, 11674 insertions, 3145 deletions
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 572b567cddf..be27bbb4283 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -241,13 +241,10 @@ describe AutocompleteController do it 'returns projects' do expect(json_response).to be_kind_of(Array) - expect(json_response.size).to eq(2) - - expect(json_response.first['id']).to eq(0) - expect(json_response.first['name_with_namespace']).to eq 'No project' + expect(json_response.size).to eq(1) - expect(json_response.last['id']).to eq authorized_project.id - expect(json_response.last['name_with_namespace']).to eq authorized_project.name_with_namespace + expect(json_response.first['id']).to eq authorized_project.id + expect(json_response.first['name_with_namespace']).to eq authorized_project.name_with_namespace end end end @@ -265,10 +262,10 @@ describe AutocompleteController do it 'returns projects' do expect(json_response).to be_kind_of(Array) - expect(json_response.size).to eq(2) + expect(json_response.size).to eq(1) - expect(json_response.last['id']).to eq authorized_search_project.id - expect(json_response.last['name_with_namespace']).to eq authorized_search_project.name_with_namespace + 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 end end end @@ -292,7 +289,7 @@ describe AutocompleteController do it 'returns projects' do expect(json_response).to be_kind_of(Array) - expect(json_response.size).to eq 3 # Of a total of 4 + expect(json_response.size).to eq 2 # Of a total of 3 end end end @@ -312,9 +309,9 @@ describe AutocompleteController do get(:projects, project_id: project.id, offset_id: authorized_project.id) end - it 'returns "No project"' do - expect(json_response.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there - expect(json_response.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either + it 'returns projects' do + expect(json_response).to be_kind_of(Array) + expect(json_response.size).to eq 2 # Of a total of 3 end end end @@ -331,10 +328,9 @@ describe AutocompleteController do get(:projects, project_id: project.id) end - it 'returns a single "No project"' do + it 'returns no projects' do expect(json_response).to be_kind_of(Array) - expect(json_response.size).to eq(1) # 'No project' - expect(json_response.first['id']).to eq 0 + expect(json_response.size).to eq(0) end end end diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb new file mode 100644 index 00000000000..c9687af4dd2 --- /dev/null +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe IssuableCollections do + let(:user) { create(:user) } + + let(:controller) do + klass = Class.new do + def self.helper_method(name); end + + include IssuableCollections + end + + controller = klass.new + + allow(controller).to receive(:params).and_return(state: 'opened') + + controller + end + + describe '#redirect_out_of_range' do + before do + allow(controller).to receive(:url_for) + end + + it 'returns true and redirects if the offset is out of range' do + relation = double(:relation, current_page: 10) + + expect(controller).to receive(:redirect_to) + expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(true) + end + + it 'returns false if the offset is not out of range' do + relation = double(:relation, current_page: 1) + + expect(controller).not_to receive(:redirect_to) + expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(false) + end + end + + describe '#issues_page_count' do + it 'returns the number of issue pages' do + project = create(:project, :public) + + create(:issue, project: project) + + finder = IssuesFinder.new(user) + issues = finder.execute + + allow(controller).to receive(:issues_finder) + .and_return(finder) + + expect(controller.send(:issues_page_count, issues)).to eq(1) + end + end + + describe '#merge_requests_page_count' do + it 'returns the number of merge request pages' do + project = create(:project, :public) + + create(:merge_request, source_project: project, target_project: project) + + finder = MergeRequestsFinder.new(user) + merge_requests = finder.execute + + allow(controller).to receive(:merge_requests_finder) + .and_return(finder) + + pages = controller.send(:merge_requests_page_count, merge_requests) + + expect(pages).to eq(1) + end + end + + describe '#page_count_for_relation' do + it 'returns the number of pages' do + relation = double(:relation, limit_value: 20) + pages = controller.send(:page_count_for_relation, relation, 28) + + expect(pages).to eq(2) + end + end +end diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb index 9d60dab12d1..b52b63e05a4 100644 --- a/spec/controllers/profiles_controller_spec.rb +++ b/spec/controllers/profiles_controller_spec.rb @@ -16,7 +16,11 @@ describe ProfilesController do end it "ignores an email update from a user with an external email address" do - ldap_user = create(:omniauth_user, external_email: true) + stub_omniauth_setting(sync_profile_from_provider: ['ldap']) + stub_omniauth_setting(sync_profile_attributes: true) + + ldap_user = create(:omniauth_user) + ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true) sign_in(ldap_user) put :update, @@ -27,5 +31,24 @@ describe ProfilesController do expect(response.status).to eq(302) expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com') end + + it "ignores an email and name update but allows a location update from a user with external email and name, but not external location" do + stub_omniauth_setting(sync_profile_from_provider: ['ldap']) + stub_omniauth_setting(sync_profile_attributes: true) + + ldap_user = create(:omniauth_user, name: 'Alex') + ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true, location_synced: false) + sign_in(ldap_user) + + put :update, + user: { email: "john@gmail.com", name: "John", location: "City, Country" } + + ldap_user.reload + + expect(response.status).to eq(302) + expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com') + expect(ldap_user.name).not_to eq('John') + expect(ldap_user.location).to eq('City, Country') + end end end diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb index d2c613a2423..caa63e7bd22 100644 --- a/spec/controllers/projects/artifacts_controller_spec.rb +++ b/spec/controllers/projects/artifacts_controller_spec.rb @@ -81,14 +81,6 @@ describe Projects::ArtifactsController do expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt')) end end - - context 'when the file does not exist' do - it 'responds Not Found' do - get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown' - - expect(response).to be_not_found - end - end end describe 'GET latest_succeeded' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 65f4d09cfce..5d9403c23ac 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -233,144 +233,119 @@ describe Projects::IssuesController do end end - context 'when moving issue to another private project' do - let(:another_project) { create(:project, :private) } - - context 'when user has access to move issue' do - before do - another_project.team << [user, :reporter] - end - - it 'moves issue to another project' do - move_issue + context 'Akismet is enabled' do + let(:project) { create(:project_empty_repo, :public) } - expect(response).to have_http_status :found - expect(another_project.issues).not_to be_empty - end + before do + stub_application_setting(recaptcha_enabled: true) + allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) end - context 'when user does not have access to move issue' do - it 'responds with 404' do - move_issue + context 'when an issue is not identified as spam' do + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) + allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false) + end - expect(response).to have_http_status :not_found + it 'normally updates the issue' do + expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo') end end - context 'Akismet is enabled' do - let(:project) { create(:project_empty_repo, :public) } - + context 'when an issue is identified as spam' do before do - stub_application_setting(recaptcha_enabled: true) - allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) end - context 'when an issue is not identified as spam' do - before do - allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false) + context 'when captcha is not verified' do + def update_spam_issue + update_issue(title: 'Spam Title', description: 'Spam lives here') end - it 'normally updates the issue' do - expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo') + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) end - end - context 'when an issue is identified as spam' do - before do - allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) + it 'rejects an issue recognized as a spam' do + expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true) + expect { update_spam_issue }.not_to change { issue.reload.title } end - context 'when captcha is not verified' do - def update_spam_issue - update_issue(title: 'Spam Title', description: 'Spam lives here') - end + it 'rejects an issue recognized as a spam when recaptcha disabled' do + stub_application_setting(recaptcha_enabled: false) - before do - allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) - end + expect { update_spam_issue }.not_to change { issue.reload.title } + end - it 'rejects an issue recognized as a spam' do - expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true) - expect { update_spam_issue }.not_to change { issue.reload.title } - end + it 'creates a spam log' do + update_spam_issue - it 'rejects an issue recognized as a spam when recaptcha disabled' do - stub_application_setting(recaptcha_enabled: false) + spam_logs = SpamLog.all - expect { update_spam_issue }.not_to change { issue.reload.title } - end + expect(spam_logs.count).to eq(1) + expect(spam_logs.first.title).to eq('Spam Title') + expect(spam_logs.first.recaptcha_verified).to be_falsey + end - it 'creates a spam log' do + context 'as HTML' do + it 'renders verify template' do update_spam_issue - spam_logs = SpamLog.all - - expect(spam_logs.count).to eq(1) - expect(spam_logs.first.title).to eq('Spam Title') - expect(spam_logs.first.recaptcha_verified).to be_falsey + expect(response).to render_template(:verify) end + end - context 'as HTML' do - it 'renders verify template' do - update_spam_issue - - expect(response).to render_template(:verify) - end + context 'as JSON' do + before do + update_issue({ title: 'Spam Title', description: 'Spam lives here' }, format: :json) end - context 'as JSON' do - before do - update_issue({ title: 'Spam Title', description: 'Spam lives here' }, format: :json) - end - - it 'renders json errors' do - expect(json_response) - .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."]) - end + it 'renders json errors' do + expect(json_response) + .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."]) + end - it 'returns 422 status' do - expect(response).to have_http_status(422) - end + it 'returns 422 status' do + expect(response).to have_http_status(422) end end + end - context 'when captcha is verified' do - let(:spammy_title) { 'Whatever' } - let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) } + context 'when captcha is verified' do + let(:spammy_title) { 'Whatever' } + let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) } - def update_verified_issue - update_issue({ title: spammy_title }, - { spam_log_id: spam_logs.last.id, - recaptcha_verification: true }) - end + def update_verified_issue + update_issue({ title: spammy_title }, + { spam_log_id: spam_logs.last.id, + recaptcha_verification: true }) + end - before do - allow_any_instance_of(described_class).to receive(:verify_recaptcha) - .and_return(true) - end + before do + allow_any_instance_of(described_class).to receive(:verify_recaptcha) + .and_return(true) + end - it 'redirect to issue page' do - update_verified_issue + it 'redirect to issue page' do + update_verified_issue - expect(response) - .to redirect_to(project_issue_path(project, issue)) - end + expect(response) + .to redirect_to(project_issue_path(project, issue)) + end - it 'accepts an issue after recaptcha is verified' do - expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title) - end + it 'accepts an issue after recaptcha is verified' do + expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title) + end - it 'marks spam log as recaptcha_verified' do - expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true) - end + it 'marks spam log as recaptcha_verified' do + expect { update_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true) + end - it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do - spam_log = create(:spam_log) + it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do + spam_log = create(:spam_log) - expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) } - .not_to change { SpamLog.last.recaptcha_verified } - end + expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) } + .not_to change { SpamLog.last.recaptcha_verified } end end end @@ -385,13 +360,45 @@ describe Projects::IssuesController do put :update, params end + end + end + + describe 'POST #move' do + before do + sign_in(user) + project.add_developer(user) + end + + context 'when moving issue to another private project' do + let(:another_project) { create(:project, :private) } + + context 'when user has access to move issue' do + before do + another_project.add_reporter(user) + end + + it 'moves issue to another project' do + move_issue + + expect(response).to have_http_status :ok + expect(another_project.issues).not_to be_empty + end + end + + context 'when user does not have access to move issue' do + it 'responds with 404' do + move_issue + + expect(response).to have_http_status :not_found + end + end def move_issue - put :update, + post :move, + format: :json, namespace_id: project.namespace.to_param, project_id: project, id: issue.iid, - issue: { title: 'New title' }, move_to_project_id: another_project.id end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index bb67db268fa..6775012bab5 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -56,6 +56,28 @@ describe Projects::MergeRequestsController do expect(response).to be_success end + + context "loads notes" do + let(:first_contributor) { create(:user) } + let(:contributor) { create(:user) } + let(:merge_request) { create(:merge_request, author: first_contributor, target_project: project, source_project: project) } + let(:contributor_merge_request) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) } + # the order here is important + # as the controller reloads these from DB, references doesn't correspond after + let!(:first_contributor_note) { create(:note, author: first_contributor, noteable: merge_request, project: project) } + let!(:contributor_note) { create(:note, author: contributor, noteable: merge_request, project: project) } + let!(:owner_note) { create(:note, author: user, noteable: merge_request, project: project) } + + it "with special_role FIRST_TIME_CONTRIBUTOR" do + go(format: :html) + + notes = assigns(:notes) + expect(notes).to match(a_collection_containing_exactly(an_object_having_attributes(special_role: Note::SpecialRole::FIRST_TIME_CONTRIBUTOR), + an_object_having_attributes(special_role: nil), + an_object_having_attributes(special_role: nil) + )) + end + end end describe 'as json' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 5bba1dec7db..c2b59239af9 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -12,6 +12,7 @@ FactoryGirl.define do started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' commands 'ls -a' + protected false options do { @@ -106,7 +107,7 @@ FactoryGirl.define do end trait :triggered do - trigger_request factory: :ci_trigger_request_with_variables + trigger_request factory: :ci_trigger_request end after(:build) do |build, evaluator| @@ -226,5 +227,9 @@ FactoryGirl.define do status 'created' self.when 'manual' end + + trait :protected do + protected true + end end end diff --git a/spec/factories/ci/pipeline_variable_variables.rb b/spec/factories/ci/pipeline_variables.rb index 7c1a7faec08..7c1a7faec08 100644 --- a/spec/factories/ci/pipeline_variable_variables.rb +++ b/spec/factories/ci/pipeline_variables.rb diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index e83a0e599a8..e5ea6b41ea3 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -4,6 +4,7 @@ FactoryGirl.define do ref 'master' sha '97de212e80737a608d939f648d959671fb0a0142' status 'pending' + protected false project @@ -59,6 +60,10 @@ FactoryGirl.define do trait :failed do status :failed end + + trait :protected do + protected true + end end end end diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index 05abf60d5ce..88bb755d068 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -5,6 +5,7 @@ FactoryGirl.define do platform "darwin" is_shared false active true + access_level :not_protected trait :online do contacted_at Time.now @@ -21,5 +22,9 @@ FactoryGirl.define do trait :inactive do active false end + + trait :ref_protected do + access_level :ref_protected + end end end diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb index 10e0ab4fd3c..40b8848920e 100644 --- a/spec/factories/ci/trigger_requests.rb +++ b/spec/factories/ci/trigger_requests.rb @@ -1,14 +1,5 @@ FactoryGirl.define do factory :ci_trigger_request, class: Ci::TriggerRequest do trigger factory: :ci_trigger - - factory :ci_trigger_request_with_variables do - variables do - { - TRIGGER_KEY_1: 'TRIGGER_VALUE_1', - TRIGGER_KEY_2: 'TRIGGER_VALUE_2' - } - end - end end end diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb index a5aeffbe12d..c0beecf0bea 100644 --- a/spec/factories/gpg_signature.rb +++ b/spec/factories/gpg_signature.rb @@ -6,6 +6,6 @@ FactoryGirl.define do project gpg_key gpg_key_primary_keyid { gpg_key.primary_keyid } - valid_signature true + verification_status :verified end end diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb index 2e5a7c805c9..8d124dc2381 100644 --- a/spec/factories/project_auto_devops.rb +++ b/spec/factories/project_auto_devops.rb @@ -2,5 +2,6 @@ FactoryGirl.define do factory :project_auto_devops do project enabled true + domain "example.com" end end diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb index 3e3404dfdac..02f50d7e27f 100644 --- a/spec/features/admin/admin_browses_logs_spec.rb +++ b/spec/features/admin/admin_browses_logs_spec.rb @@ -8,8 +8,10 @@ describe 'Admin browses logs' do it 'shows available log files' do visit admin_logs_path - expect(page).to have_content 'test.log' - expect(page).to have_content 'githost.log' - expect(page).to have_content 'application.log' + expect(page).to have_link 'application.log' + expect(page).to have_link 'githost.log' + expect(page).to have_link 'test.log' + expect(page).to have_link 'sidekiq.log' + expect(page).to have_link 'repocheck.log' end end diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index a6ad5981f8f..c480b5b7e34 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -8,8 +8,8 @@ describe 'Issue Boards add issue modal', :js do let!(:label) { create(:label, project: project) } let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list2) { create(:list, board: board, label: label, position: 1) } - let!(:issue) { create(:issue, project: project) } - let!(:issue2) { create(:issue, project: project) } + let!(:issue) { create(:issue, project: project, title: 'abc', description: 'def') } + let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') } before do project.team << [user, :master] diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 913258ca40f..e010b5f3444 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -73,15 +73,15 @@ describe 'Issue Boards', js: true do let!(:list2) { create(:list, board: board, label: development, position: 1) } let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } - let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) } - let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) } - let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) } - let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) } - let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) } - let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) } - let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) } - let!(:issue8) { create(:closed_issue, project: project) } - let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) } + let!(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) } + let!(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) } + let!(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) } + let!(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) } + let!(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) } + let!(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) } + let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) } + let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') } + let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) } before do visit project_board_path(project, board) diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 0c9fcc60d30..479fb713297 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -203,105 +203,4 @@ describe 'Commits' do end end end - - describe 'GPG signed commits', :js do - it 'changes from unverified to verified when the user changes his email to match the gpg key' do - user = create :user, email: 'unrelated.user@example.org' - project.team << [user, :master] - - Sidekiq::Testing.inline! do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - sign_in(user) - - visit project_commits_path(project, :'signed-commits') - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).not_to have_content 'Verified' - end - - # user changes his email which makes the gpg key verified - Sidekiq::Testing.inline! do - user.skip_reconfirmation! - user.update_attributes!(email: GpgHelpers::User1.emails.first) - end - - visit project_commits_path(project, :'signed-commits') - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' - end - end - - it 'changes from unverified to verified when the user adds the missing gpg key' do - user = create :user, email: GpgHelpers::User1.emails.first - project.team << [user, :master] - - sign_in(user) - - visit project_commits_path(project, :'signed-commits') - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).not_to have_content 'Verified' - end - - # user adds the gpg key which makes the signature valid - Sidekiq::Testing.inline! do - create :gpg_key, key: GpgHelpers::User1.public_key, user: user - end - - visit project_commits_path(project, :'signed-commits') - - within '#commits-list' do - expect(page).to have_content 'Unverified' - expect(page).to have_content 'Verified' - end - end - - it 'shows popover badges' do - gpg_user = create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard' - Sidekiq::Testing.inline! do - create :gpg_key, key: GpgHelpers::User1.public_key, user: gpg_user - end - - user = create :user - project.team << [user, :master] - - sign_in(user) - visit project_commits_path(project, :'signed-commits') - - # unverified signature - click_on 'Unverified', match: :first - within '.popover' do - expect(page).to have_content 'This commit was signed with an unverified signature.' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" - end - - # verified and the gpg user has a gitlab profile - click_on 'Verified', match: :first - within '.popover' do - expect(page).to have_content 'This commit was signed with a verified signature.' - expect(page).to have_content 'Nannie Bernhard' - expect(page).to have_content '@nannie.bernhard' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" - end - - # verified and the gpg user's profile doesn't exist anymore - gpg_user.destroy! - - visit project_commits_path(project, :'signed-commits') - - click_on 'Verified', match: :first - within '.popover' do - expect(page).to have_content 'This commit was signed with a verified signature.' - expect(page).to have_content 'Nannie Bernhard' - expect(page).to have_content 'nannie.bernhard@example.com' - expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" - end - end - end end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 4297bfff3d9..2db6f9a2982 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -166,12 +166,10 @@ describe 'New/edit issue', :js do end end - page.within '.issuable-meta' do + page.within '.breadcrumbs' do issue = Issue.find_by(title: 'title') - expect(page).to have_text("Issue #{issue.to_reference}") - # compare paths because the host differ in test - expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue)) + expect(page).to have_text("Issues #{issue.to_reference}") end end diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index c470cb7c716..28b636f9359 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -40,18 +40,4 @@ feature 'Issue Detail', :js do end end end - - context 'when authored by a user who is later deleted' do - before do - issue.update_attribute(:author_id, nil) - sign_in(user) - visit project_issue_path(project, issue) - end - - it 'shows the issue' do - page.within('.issuable-details') do - expect(find('h2')).to have_content(issue.title) - end - end - end end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 494c309c9ea..b2724945da4 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -15,11 +15,11 @@ feature 'issue move to another project' do background do old_project.team << [user, :guest] - edit_issue(issue) + visit issue_path(issue) end scenario 'moving issue to another project not allowed' do - expect(page).to have_no_selector('#move_to_project_id') + expect(page).to have_no_selector('.js-sidebar-move-issue-block') end end @@ -34,12 +34,14 @@ feature 'issue move to another project' do old_project.team << [user, :reporter] new_project.team << [user, :reporter] - edit_issue(issue) + visit issue_path(issue) end scenario 'moving issue to another project', js: true do - find('#issuable-move', visible: false).set(new_project.id) - click_button('Save changes') + find('.js-move-issue').trigger('click') + wait_for_requests + all('.js-move-issue-dropdown-item')[0].click + find('.js-move-issue-confirmation-button').click expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}") expect(page).to have_content("moved from #{cross_reference}#{issue.to_reference}") @@ -50,13 +52,12 @@ feature 'issue move to another project' do scenario 'searching project dropdown', js: true do new_project_search.team << [user, :reporter] - page.within '.detail-page-description' do - first('.select2-choice').click - end + find('.js-move-issue').trigger('click') + wait_for_requests - fill_in('s2id_autogen1_search', with: new_project_search.name) + page.within '.js-sidebar-move-issue-block' do + fill_in('sidebar-move-issue-dropdown-search', with: new_project_search.name) - page.within '.select2-drop' do expect(page).to have_content(new_project_search.name) expect(page).not_to have_content(new_project.name) end @@ -68,10 +69,10 @@ feature 'issue move to another project' do background { another_project.team << [user, :guest] } scenario 'browsing projects in projects select' do - click_link 'Move to a different project' + find('.js-move-issue').trigger('click') + wait_for_requests - page.within '.select2-results' do - expect(page).to have_content 'No project' + page.within '.js-sidebar-move-issue-block' do expect(page).to have_content new_project.name_with_namespace end end @@ -89,11 +90,6 @@ feature 'issue move to another project' do end end - def edit_issue(issue) - visit issue_path(issue) - page.within('.issuable-actions') { first(:link, 'Edit').click } - end - def issue_path(issue) project_issue_path(issue.project, issue) end diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index e77f1f92731..ca536f2800c 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -139,7 +139,7 @@ feature 'Diff note avatars', js: true do end page.within find("[id='#{position.line_code(project.repository)}']") do - find('.diff-notes-collapse').click + find('.diff-notes-collapse').trigger('click') expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) end @@ -152,7 +152,7 @@ feature 'Diff note avatars', js: true do page.within '.js-discussion-note-form' do find('.js-note-text').native.send_keys('Test') - find('.js-comment-button').trigger 'click' + find('.js-comment-button').trigger('click') wait_for_requests end diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index 89410b0e90f..de98b147d04 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -84,13 +84,10 @@ describe 'New/edit merge request', :js do end end - page.within '.issuable-meta' do + page.within '.breadcrumbs' do merge_request = MergeRequest.find_by(source_branch: 'fix') - expect(page).to have_text("Merge request #{merge_request.to_reference}") - # compare paths because the host differ in test - expect(find_link(merge_request.to_reference)[:href]) - .to end_with(merge_request_path(merge_request)) + expect(page).to have_text("Merge Requests #{merge_request.to_reference}") end end diff --git a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb new file mode 100644 index 00000000000..55a82bdf2b9 --- /dev/null +++ b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +feature 'Resolve outdated diff discussions', js: true do + let(:project) { create(:project, :repository, :public) } + + let(:merge_request) do + create(:merge_request, source_project: project, source_branch: 'csv', target_branch: 'master') + end + + let(:outdated_diff_refs) { project.commit('926c6595b263b2a40da6b17f3e3b7ea08344fad6').diff_refs } + let(:current_diff_refs) { merge_request.diff_refs } + + let(:outdated_position) do + Gitlab::Diff::Position.new( + old_path: 'files/csv/Book1.csv', + new_path: 'files/csv/Book1.csv', + old_line: nil, + new_line: 9, + diff_refs: outdated_diff_refs + ) + end + + let(:current_position) do + Gitlab::Diff::Position.new( + old_path: 'files/csv/Book1.csv', + new_path: 'files/csv/Book1.csv', + old_line: nil, + new_line: 1, + diff_refs: current_diff_refs + ) + end + + let!(:outdated_discussion) do + create(:diff_note_on_merge_request, + project: project, + noteable: merge_request, + position: outdated_position).to_discussion + end + + let!(:current_discussion) do + create(:diff_note_on_merge_request, + noteable: merge_request, + project: project, + position: current_position).to_discussion + end + + before do + sign_in(merge_request.author) + end + + context 'when a discussion was resolved by a push' do + before do + project.update!(resolve_outdated_diff_discussions: true) + + merge_request.update_diff_discussion_positions( + old_diff_refs: outdated_diff_refs, + new_diff_refs: current_diff_refs, + current_user: merge_request.author + ) + + visit project_merge_request_path(project, merge_request) + end + + it 'shows that as automatically resolved' do + within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do + expect(page).to have_css('.discussion-body', visible: false) + expect(page).to have_content('Automatically resolved') + end + end + + it 'does not show that for active discussions' do + within(".discussion[data-discussion-id='#{current_discussion.id}']") do + expect(page).to have_css('.discussion-body', visible: true) + expect(page).not_to have_content('Automatically resolved') + end + end + end +end diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb index 877f305120e..442ce14eb7e 100644 --- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb @@ -97,6 +97,16 @@ feature 'Merge requests > User posts diff notes', :js do visit diffs_project_merge_request_path(project, merge_request, view: 'inline') end + context 'after deleteing a note' do + it 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + + first('.js-note-delete', visible: false).trigger('click') + + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) + end + end + context 'with a new line' do it 'allows commenting' do should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index 6edc482b47e..623e4f341c5 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -42,7 +42,7 @@ feature 'Profile > GPG Keys' do scenario 'User revokes a key via the key index' do gpg_key = create :gpg_key, user: user, key: GpgHelpers::User2.public_key - gpg_signature = create :gpg_signature, gpg_key: gpg_key, valid_signature: true + gpg_signature = create :gpg_signature, gpg_key: gpg_key, verification_status: :verified visit profile_gpg_keys_path @@ -51,7 +51,7 @@ feature 'Profile > GPG Keys' do expect(page).to have_content('Your GPG keys (0)') expect(gpg_signature.reload).to have_attributes( - valid_signature: false, + verification_status: 'unknown_key', gpg_key: nil ) end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 2eb6fab129d..ad2db1a34f4 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -18,23 +18,25 @@ feature 'Import/Export - project import integration test', js: true do context 'when selecting the namespace' do let(:user) { create(:admin) } - let!(:namespace) { create(:namespace, name: "asd", owner: user) } + let!(:namespace) { create(:namespace, name: 'asd', owner: user) } + let(:project_path) { 'test-project-path' + SecureRandom.hex } context 'prefilled the path' do scenario 'user imports an exported project successfully' do visit new_project_path select2(namespace.id, from: '#project_namespace_id') - fill_in :project_path, with: 'test-project-path', visible: true + fill_in :project_path, with: project_path, visible: true click_link 'GitLab export' expect(page).to have_content('Import an exported GitLab project') - expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path") - expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\z/).and_call_original + expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") + expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\h*\z/).and_call_original attach_file('file', file) + click_on 'Import project' - expect { click_on 'Import project' }.to change { Project.count }.by(1) + expect(Project.count).to eq(1) project = Project.last expect(project).not_to be_nil @@ -64,7 +66,7 @@ feature 'Import/Export - project import integration test', js: true do end scenario 'invalid project' do - namespace = create(:namespace, name: "asd", owner: user) + namespace = create(:namespace, name: 'asdf', owner: user) project = create(:project, namespace: namespace) visit new_project_path diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 037ac00d39f..3b5c6966287 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -292,26 +292,44 @@ feature 'Jobs' do end feature 'Variables' do - let(:trigger_request) { create(:ci_trigger_request_with_variables) } + let(:trigger_request) { create(:ci_trigger_request) } let(:job) do create :ci_build, pipeline: pipeline, trigger_request: trigger_request end - before do - visit project_job_path(project, job) + shared_examples 'expected variables behavior' do + it 'shows variable key and value after click', js: true do + expect(page).to have_css('.reveal-variables') + expect(page).not_to have_css('.js-build-variable') + expect(page).not_to have_css('.js-build-value') + + click_button 'Reveal Variables' + + expect(page).not_to have_css('.reveal-variables') + expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1') + expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1') + end end - it 'shows variable key and value after click', js: true do - expect(page).to have_css('.reveal-variables') - expect(page).not_to have_css('.js-build-variable') - expect(page).not_to have_css('.js-build-value') + context 'when variables are stored in trigger_request' do + before do + trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } ) - click_button 'Reveal Variables' + visit project_job_path(project, job) + end + + it_behaves_like 'expected variables behavior' + end + + context 'when variables are stored in pipeline_variables' do + before do + create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1') + + visit project_job_path(project, job) + end - expect(page).not_to have_css('.reveal-variables') - expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1') - expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1') + it_behaves_like 'expected variables behavior' end end diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb index b2b39dbd24c..eb2d3ff50a0 100644 --- a/spec/features/projects/sub_group_issuables_spec.rb +++ b/spec/features/projects/sub_group_issuables_spec.rb @@ -26,7 +26,6 @@ describe 'Subgroup Issuables', :js, :nested_groups do def expect_to_have_full_subgroup_title title = find('.breadcrumbs-links') - expect(title).not_to have_selector '.initializing' - expect(title).to have_content 'group / subgroup / project' + expect(title).to have_content 'group subgroup project' end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index baf3d29e6c5..81f7ab80a04 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -95,49 +95,6 @@ feature 'Project' do end end - describe 'project title' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } - - before do - sign_in(user) - project.add_user(user, Gitlab::Access::MASTER) - visit project_path(project) - end - - it 'clicks toggle and shows dropdown', js: true do - find('.js-projects-dropdown-toggle').click - expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1) - end - end - - describe 'project title' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } - let(:project2) { create(:project, namespace: user.namespace, path: 'test') } - let(:issue) { create(:issue, project: project) } - - context 'on issues page', js: true do - before do - sign_in(user) - project.add_user(user, Gitlab::Access::MASTER) - project2.add_user(user, Gitlab::Access::MASTER) - visit project_issue_path(project, issue) - end - - it 'clicks toggle and shows dropdown' do - find('.js-projects-dropdown-toggle').click - expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2) - - page.within '.dropdown-menu-projects' do - click_link project.name_with_namespace - end - - expect(page).to have_content project.name - end - end - end - describe 'tree view (default view is set to Files)' do let(:user) { create(:user, project_view: 'files') } let(:project) { create(:forked_project_with_submodules) } diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 785cfeb34bd..c7f0e342809 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -43,6 +43,21 @@ feature 'Runners' do expect(page).not_to have_content(specific_runner.display_name) end + scenario 'user edits the runner to be protected' do + visit runners_path(project) + + within '.activated-specific-runners' do + first('.edit-runner > a').click + end + + expect(page.find_field('runner[access_level]')).not_to be_checked + + check 'runner_access_level' + click_button 'Save changes' + + expect(page).to have_content 'Protected Yes' + end + context 'when a runner has a tag' do background do specific_runner.update(tag_list: ['tag']) diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb new file mode 100644 index 00000000000..8efa5b58141 --- /dev/null +++ b/spec/features/signed_commits_spec.rb @@ -0,0 +1,179 @@ +require 'spec_helper' + +describe 'GPG signed commits', :js do + let(:project) { create(:project, :repository) } + + it 'changes from unverified to verified when the user changes his email to match the gpg key' do + user = create :user, email: 'unrelated.user@example.org' + project.team << [user, :master] + + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + sign_in(user) + + visit project_commits_path(project, :'signed-commits') + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).not_to have_content 'Verified' + end + + # user changes his email which makes the gpg key verified + Sidekiq::Testing.inline! do + user.skip_reconfirmation! + user.update_attributes!(email: GpgHelpers::User1.emails.first) + end + + visit project_commits_path(project, :'signed-commits') + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).to have_content 'Verified' + end + end + + it 'changes from unverified to verified when the user adds the missing gpg key' do + user = create :user, email: GpgHelpers::User1.emails.first + project.team << [user, :master] + + sign_in(user) + + visit project_commits_path(project, :'signed-commits') + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).not_to have_content 'Verified' + end + + # user adds the gpg key which makes the signature valid + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + visit project_commits_path(project, :'signed-commits') + + within '#commits-list' do + expect(page).to have_content 'Unverified' + expect(page).to have_content 'Verified' + end + end + + context 'shows popover badges' do + let(:user_1) do + create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard' + end + + let(:user_1_key) do + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user_1 + end + end + + let(:user_2) do + create(:user, email: GpgHelpers::User2.emails.first, username: 'bette.cartwright', name: 'Bette Cartwright').tap do |user| + # secondary, unverified email + create :email, user: user, email: GpgHelpers::User2.emails.last + end + end + + let(:user_2_key) do + Sidekiq::Testing.inline! do + create :gpg_key, key: GpgHelpers::User2.public_key, user: user_2 + end + end + + before do + user = create :user + project.team << [user, :master] + + sign_in(user) + end + + it 'unverified signature' do + visit project_commits_path(project, :'signed-commits') + + within(find('.commit', text: 'signed commit by bette cartwright')) do + click_on 'Unverified' + within '.popover' do + expect(page).to have_content 'This commit was signed with an unverified signature.' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" + end + end + end + + it 'unverified signature: user email does not match the committer email, but is the same user' do + user_2_key + + visit project_commits_path(project, :'signed-commits') + + within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do + click_on 'Unverified' + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.' + expect(page).to have_content 'Bette Cartwright' + expect(page).to have_content '@bette.cartwright' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" + end + end + end + + it 'unverified signature: user email does not match the committer email' do + user_2_key + + visit project_commits_path(project, :'signed-commits') + + within(find('.commit', text: 'signed commit by bette cartwright')) do + click_on 'Unverified' + within '.popover' do + expect(page).to have_content "This commit was signed with a different user's verified signature." + expect(page).to have_content 'Bette Cartwright' + expect(page).to have_content '@bette.cartwright' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}" + end + end + end + + it 'verified and the gpg user has a gitlab profile' do + user_1_key + + visit project_commits_path(project, :'signed-commits') + + within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do + click_on 'Verified' + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' + expect(page).to have_content 'Nannie Bernhard' + expect(page).to have_content '@nannie.bernhard' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" + end + end + end + + it "verified and the gpg user's profile doesn't exist anymore" do + user_1_key + + visit project_commits_path(project, :'signed-commits') + + # wait for the signature to get generated + within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do + expect(page).to have_content 'Verified' + end + + user_1.destroy! + + refresh + + within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do + click_on 'Verified' + within '.popover' do + expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.' + expect(page).to have_content 'Nannie Bernhard' + expect(page).to have_content 'nannie.bernhard@example.com' + expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}" + end + end + end + end +end diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index ff6f71d7528..aeb0534b733 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -200,9 +200,9 @@ feature 'Task Lists' do visit_issue(project, issue) expect(page).to have_selector('.js-task-list-container') - logout(:user) + gitlab_sign_out - login_as(user2) + gitlab_sign_in(user2) visit current_path expect(page).not_to have_selector('.js-task-list-container') end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 0e80df94e18..47b173dea0a 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -15,8 +15,8 @@ describe IssuesFinder do set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) } describe '#execute' do - set(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } - set(:label_link) { create(:label_link, label: label, target: issue2) } + let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: issue2) } let(:search_user) { user } let(:params) { {} } let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } @@ -347,6 +347,20 @@ describe IssuesFinder do end end + describe '#row_count', :request_store do + it 'returns the number of rows for the default state' do + finder = described_class.new(user) + + expect(finder.row_count).to eq(3) + end + + it 'returns the number of rows for a given state' do + finder = described_class.new(user, state: 'closed') + + expect(finder.row_count).to be_zero + end + end + describe '#with_confidentiality_access_check' do let(:guest) { create(:user) } set(:authorized_user) { create(:user) } diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index b54155a6704..95f445e7905 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -108,4 +108,18 @@ describe MergeRequestsFinder do end end end + + describe '#row_count', :request_store do + it 'returns the number of rows for the default state' do + finder = described_class.new(user) + + expect(finder.row_count).to eq(3) + end + + it 'returns the number of rows for a given state' do + finder = described_class.new(user, state: 'closed') + + expect(finder.row_count).to eq(1) + end + end end diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json index f6346bd0fb6..c76c6945117 100644 --- a/spec/fixtures/api/schemas/pipeline_schedule.json +++ b/spec/fixtures/api/schemas/pipeline_schedule.json @@ -31,6 +31,10 @@ "web_url": { "type": "uri" } }, "additionalProperties": false + }, + "variables": { + "type": ["array", "null"], + "items": { "$ref": "pipeline_schedule_variable.json" } } }, "required": [ diff --git a/spec/fixtures/api/schemas/pipeline_schedule_variable.json b/spec/fixtures/api/schemas/pipeline_schedule_variable.json new file mode 100644 index 00000000000..f7ccb2d44a0 --- /dev/null +++ b/spec/fixtures/api/schemas/pipeline_schedule_variable.json @@ -0,0 +1,8 @@ +{ + "type": ["object", "null"], + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/helpers/blame_helper_spec.rb b/spec/helpers/blame_helper_spec.rb index b4368516d83..722d21c566f 100644 --- a/spec/helpers/blame_helper_spec.rb +++ b/spec/helpers/blame_helper_spec.rb @@ -35,25 +35,32 @@ describe BlameHelper do end describe '#age_map_class' do - let(:dates) do - [Time.zone.local(2014, 3, 17, 0, 0, 0)] - end - let(:blame_groups) do - [ - { commit: double(committed_date: dates[0]) } - ] - end + let(:date) { Time.zone.local(2014, 3, 17, 0, 0, 0) } + let(:blame_groups) { [{ commit: double(committed_date: date) }] } let(:duration) do - project = double(created_at: dates[0]) + project = double(created_at: date) helper.age_map_duration(blame_groups, project) end it 'returns blame-commit-age-9 when oldest' do - expect(helper.age_map_class(dates[0], duration)).to eq 'blame-commit-age-9' + expect(helper.age_map_class(date, duration)).to eq 'blame-commit-age-9' end it 'returns blame-commit-age-0 class when newest' do expect(helper.age_map_class(duration[:now], duration)).to eq 'blame-commit-age-0' end + + context 'when called on the same day as project creation' do + let(:same_day_duration) do + project = double(created_at: now) + helper.age_map_duration(today_blame_groups, project) + end + let(:today_blame_groups) { [{ commit: double(committed_date: now) }] } + let(:now) { Time.zone.now } + + it 'returns blame-commit-age-0 class' do + expect(helper.age_map_class(duration[:now], same_day_duration)).to eq 'blame-commit-age-0' + end + end end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 9d6e03e3868..05f969904f5 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -91,7 +91,8 @@ describe GroupsHelper do let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } it 'outputs the groups in the correct order' do - expect(helper.group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/) + expect(helper.group_title(very_deep_nested_group)) + .to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m) end end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index dc3100311f8..ddf881a7b6f 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -58,16 +58,6 @@ describe IssuesHelper do end end - describe "merge_requests_sentence" do - subject { merge_requests_sentence(merge_requests)} - let(:merge_requests) do - [build(:merge_request, iid: 1), build(:merge_request, iid: 2), - build(:merge_request, iid: 3)] - end - - it { is_expected.to eq("!1, !2, or !3") } - end - describe '#award_user_list' do it "returns a comma-separated list of the first X users" do user = build_stubbed(:user, name: 'Joe') diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index 70eb01c9c44..03d706062b7 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -52,12 +52,71 @@ describe MarkupHelper do end end - describe '#link_to_gfm' do + describe '#markdown_field' do + let(:attribute) { :title } + + describe 'with already redacted attribute' do + it 'returns the redacted attribute' do + commit.redacted_title_html = 'commit title' + + expect(Banzai).not_to receive(:render_field) + + expect(helper.markdown_field(commit, attribute)).to eq('commit title') + end + end + + describe 'without redacted attribute' do + it 'renders the markdown value' do + expect(Banzai).to receive(:render_field).with(commit, attribute).and_call_original + + helper.markdown_field(commit, attribute) + end + end + end + + describe '#link_to_markdown_field' do + let(:link) { '/commits/0a1b2c3d' } + let(:issues) { create_list(:issue, 2, project: project) } + + it 'handles references nested in links with all the text' do + allow(commit).to receive(:title).and_return("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real") + + actual = helper.link_to_markdown_field(commit, :title, link) + doc = Nokogiri::HTML.parse(actual) + + # Make sure we didn't create invalid markup + expect(doc.errors).to be_empty + + # Leading commit link + expect(doc.css('a')[0].attr('href')).to eq link + expect(doc.css('a')[0].text).to eq 'This should finally fix ' + + # First issue link + expect(doc.css('a')[1].attr('href')) + .to eq project_issue_path(project, issues[0]) + expect(doc.css('a')[1].text).to eq issues[0].to_reference + + # Internal commit link + expect(doc.css('a')[2].attr('href')).to eq link + expect(doc.css('a')[2].text).to eq ' and ' + + # Second issue link + expect(doc.css('a')[3].attr('href')) + .to eq project_issue_path(project, issues[1]) + expect(doc.css('a')[3].text).to eq issues[1].to_reference + + # Trailing commit link + expect(doc.css('a')[4].attr('href')).to eq link + expect(doc.css('a')[4].text).to eq ' for real' + end + end + + describe '#link_to_markdown' do let(:link) { '/commits/0a1b2c3d' } let(:issues) { create_list(:issue, 2, project: project) } it 'handles references nested in links with all the text' do - actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link) + actual = helper.link_to_markdown("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link) doc = Nokogiri::HTML.parse(actual) # Make sure we didn't create invalid markup @@ -87,7 +146,7 @@ describe MarkupHelper do end it 'forwards HTML options' do - actual = helper.link_to_gfm("Fixed in #{commit.id}", link, class: 'foo') + actual = helper.link_to_markdown("Fixed in #{commit.id}", link, class: 'foo') doc = Nokogiri::HTML.parse(actual) expect(doc.css('a')).to satisfy do |v| @@ -98,23 +157,43 @@ describe MarkupHelper do it "escapes HTML passed in as the body" do actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}" - expect(helper.link_to_gfm(actual, link)) + expect(helper.link_to_markdown(actual, link)) .to match('<h1>test</h1>') end it 'ignores reference links when they are the entire body' do text = issues[0].to_reference - act = helper.link_to_gfm(text, '/foo') + act = helper.link_to_markdown(text, '/foo') expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>) end it 'replaces commit message with emoji to link' do - actual = link_to_gfm(':book: Book', '/foo') + actual = link_to_markdown(':book: Book', '/foo') expect(actual) .to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>' end end + describe '#link_to_html' do + it 'wraps the rendered content in a link' do + link = '/commits/0a1b2c3d' + issue = create(:issue, project: project) + + rendered = helper.markdown("This should finally fix #{issue.to_reference} for real", pipeline: :single_line) + doc = Nokogiri::HTML.parse(rendered) + + expect(doc.css('a')[0].attr('href')) + .to eq project_issue_path(project, issue) + expect(doc.css('a')[0].text).to eq issue.to_reference + + wrapped = helper.link_to_html(rendered, link) + doc = Nokogiri::HTML.parse(wrapped) + + expect(doc.css('a')[0].attr('href')).to eq link + expect(doc.css('a')[0].text).to eq 'This should finally fix ' + end + end + describe '#render_wiki_content' do before do @wiki = double('WikiPage') diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 9921ca1af33..cd15e27b497 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -23,10 +23,10 @@ describe NotesHelper do end describe "#notes_max_access_for_users" do - it 'returns human access levels' do - expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') - expect(helper.note_max_access_for_user(master_note)).to eq('Master') - expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter') + it 'returns access levels' do + expect(helper.note_max_access_for_user(owner_note)).to eq(Gitlab::Access::OWNER) + expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER) + expect(helper.note_max_access_for_user(reporter_note)).to eq(Gitlab::Access::REPORTER) end it 'handles access in different projects' do @@ -34,8 +34,8 @@ describe NotesHelper do second_project.team << [master, :reporter] other_note = create(:note, author: master, project: second_project) - expect(helper.note_max_access_for_user(master_note)).to eq('Master') - expect(helper.note_max_access_for_user(other_note)).to eq('Reporter') + expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER) + expect(helper.note_max_access_for_user(other_note)).to eq(Gitlab::Access::REPORTER) end end @@ -231,7 +231,7 @@ describe NotesHelper do end end - describe '#form_resurces' do + describe '#form_resources' do it 'returns note for personal snippet' do @snippet = create(:personal_snippet) @note = create(:note_on_personal_snippet) @@ -266,4 +266,22 @@ describe NotesHelper do expect(noteable_note_url(note)).to match("/#{project.namespace.path}/#{project.path}/issues/#{issue.iid}##{dom_id(note)}") end end + + describe '#discussion_resolved_intro' do + context 'when the discussion was resolved by a push' do + let(:discussion) { double(:discussion, resolved_by_push?: true) } + + it 'returns "Automatically resolved"' do + expect(discussion_resolved_intro(discussion)).to eq('Automatically resolved') + end + end + + context 'when the discussion was not resolved by a push' do + let(:discussion) { double(:discussion, resolved_by_push?: false) } + + it 'returns "Resolved"' do + expect(discussion_resolved_intro(discussion)).to eq('Resolved') + end + end + end end diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb index b33b3f3a228..c1d0614c79e 100644 --- a/spec/helpers/profiles_helper_spec.rb +++ b/spec/helpers/profiles_helper_spec.rb @@ -6,22 +6,41 @@ describe ProfilesHelper do user = create(:user) allow(helper).to receive(:current_user).and_return(user) - expect(helper.email_provider_label).to be_nil + expect(helper.attribute_provider_label(:email)).to be_nil end - it "returns omniauth provider label for users with external email" do + it "returns omniauth provider label for users with external attributes" do + stub_omniauth_setting(sync_profile_from_provider: ['cas3']) + stub_omniauth_setting(sync_profile_attributes: true) stub_cas_omniauth_provider - cas_user = create(:omniauth_user, provider: 'cas3', external_email: true, email_provider: 'cas3') + cas_user = create(:omniauth_user, provider: 'cas3') + cas_user.create_user_synced_attributes_metadata(provider: 'cas3', name_synced: true, email_synced: true, location_synced: true) allow(helper).to receive(:current_user).and_return(cas_user) - expect(helper.email_provider_label).to eq('CAS') + expect(helper.attribute_provider_label(:email)).to eq('CAS') + expect(helper.attribute_provider_label(:name)).to eq('CAS') + expect(helper.attribute_provider_label(:location)).to eq('CAS') + end + + it "returns the correct omniauth provider label for users with some external attributes" do + stub_omniauth_setting(sync_profile_from_provider: ['cas3']) + stub_omniauth_setting(sync_profile_attributes: true) + stub_cas_omniauth_provider + cas_user = create(:omniauth_user, provider: 'cas3') + cas_user.create_user_synced_attributes_metadata(provider: 'cas3', name_synced: false, email_synced: true, location_synced: false) + allow(helper).to receive(:current_user).and_return(cas_user) + + expect(helper.attribute_provider_label(:name)).to be_nil + expect(helper.attribute_provider_label(:email)).to eq('CAS') + expect(helper.attribute_provider_label(:location)).to be_nil end it "returns 'LDAP' for users with external email but no email provider" do - ldap_user = create(:omniauth_user, external_email: true) + ldap_user = create(:omniauth_user) + ldap_user.create_user_synced_attributes_metadata(email_synced: true) allow(helper).to receive(:current_user).and_return(ldap_user) - expect(helper.email_provider_label).to eq('LDAP') + expect(helper.attribute_provider_label(:email)).to eq('LDAP') end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 463af15930d..ab647401e14 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -17,7 +17,7 @@ describe SearchHelper do end end - context "with a user" do + context "with a standard user" do let(:user) { create(:user) } before do @@ -29,7 +29,11 @@ describe SearchHelper do end it "includes default sections" do - expect(search_autocomplete_opts("adm").size).to eq(1) + expect(search_autocomplete_opts("dash").size).to eq(1) + end + + it "does not include admin sections" do + expect(search_autocomplete_opts("admin").size).to eq(0) end it "does not allow regular expression in search term" do @@ -67,6 +71,18 @@ describe SearchHelper do end end end + + context 'with an admin user' do + let(:admin) { create(:admin) } + + before do + allow(self).to receive(:current_user).and_return(admin) + end + + it "includes admin sections" do + expect(search_autocomplete_opts("admin").size).to eq(1) + end + end end describe 'search_filter_input_options' do diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 8c68ceff914..2aa4fb1f6c6 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -101,12 +101,13 @@ describe('Api', () => { it('fetches projects with membership when logged in', (done) => { const query = 'dummy query'; const options = { unused: 'option' }; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; window.gon.current_user_id = 1; const expectedData = Object.assign({ search: query, per_page: 20, membership: true, + simple: true, }, options); spyOn(jQuery, 'ajax').and.callFake((request) => { expect(request.url).toEqual(expectedUrl); @@ -124,10 +125,11 @@ describe('Api', () => { it('fetches projects without membership when not logged in', (done) => { const query = 'dummy query'; const options = { unused: 'option' }; - const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; const expectedData = Object.assign({ search: query, per_page: 20, + simple: true, }, options); spyOn(jQuery, 'ajax').and.callFake((request) => { expect(request.url).toEqual(expectedUrl); diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js new file mode 100644 index 00000000000..114d282e48a --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js @@ -0,0 +1,219 @@ +import Cookies from 'js-cookie'; +import { + getCookieName, + getSelector, + showPopover, + hidePopover, + dismiss, + mouseleave, + mouseenter, + setupDismissButton, +} from '~/feature_highlight/feature_highlight_helper'; + +describe('feature highlight helper', () => { + describe('getCookieName', () => { + it('returns `feature-highlighted-` prefix', () => { + const cookieId = 'cookieId'; + expect(getCookieName(cookieId)).toEqual(`feature-highlighted-${cookieId}`); + }); + }); + + describe('getSelector', () => { + it('returns js-feature-highlight selector', () => { + const highlightId = 'highlightId'; + expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`); + }); + }); + + describe('showPopover', () => { + it('returns true when popover is shown', () => { + const context = { + hasClass: () => false, + popover: () => {}, + addClass: () => {}, + }; + + expect(showPopover.call(context)).toEqual(true); + }); + + it('returns false when popover is already shown', () => { + const context = { + hasClass: () => true, + }; + + expect(showPopover.call(context)).toEqual(false); + }); + + it('shows popover', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + addClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('show'); + done(); + }); + + showPopover.call(context); + }); + + it('adds disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + addClass: () => {}, + }; + + spyOn(context, 'addClass').and.callFake((classNames) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + done(); + }); + + showPopover.call(context); + }); + }); + + describe('hidePopover', () => { + it('returns true when popover is hidden', () => { + const context = { + hasClass: () => true, + popover: () => {}, + removeClass: () => {}, + }; + + expect(hidePopover.call(context)).toEqual(true); + }); + + it('returns false when popover is already hidden', () => { + const context = { + hasClass: () => false, + }; + + expect(hidePopover.call(context)).toEqual(false); + }); + + it('hides popover', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + removeClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('hide'); + done(); + }); + + hidePopover.call(context); + }); + + it('removes disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + removeClass: () => {}, + }; + + spyOn(context, 'removeClass').and.callFake((classNames) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + done(); + }); + + hidePopover.call(context); + }); + }); + + describe('dismiss', () => { + const context = { + hide: () => {}, + }; + + beforeEach(() => { + spyOn(Cookies, 'set').and.callFake(() => {}); + spyOn(hidePopover, 'call').and.callFake(() => {}); + spyOn(context, 'hide').and.callFake(() => {}); + dismiss.call(context); + }); + + it('sets cookie to true', () => { + expect(Cookies.set).toHaveBeenCalled(); + }); + + it('calls hide popover', () => { + expect(hidePopover.call).toHaveBeenCalled(); + }); + + it('calls hide', () => { + expect(context.hide).toHaveBeenCalled(); + }); + }); + + describe('mouseleave', () => { + it('calls hide popover if .popover:hover is false', () => { + const fakeJquery = { + length: 0, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(hidePopover, 'call'); + mouseleave(); + expect(hidePopover.call).toHaveBeenCalled(); + }); + + it('does not call hide popover if .popover:hover is true', () => { + const fakeJquery = { + length: 1, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(hidePopover, 'call'); + mouseleave(); + expect(hidePopover.call).not.toHaveBeenCalled(); + }); + }); + + describe('mouseenter', () => { + const context = {}; + + it('shows popover', () => { + spyOn(showPopover, 'call').and.returnValue(false); + mouseenter.call(context); + expect(showPopover.call).toHaveBeenCalled(); + }); + + it('registers mouseleave event if popover is showed', (done) => { + spyOn(showPopover, 'call').and.returnValue(true); + spyOn($.fn, 'on').and.callFake((eventName) => { + expect(eventName).toEqual('mouseleave'); + done(); + }); + mouseenter.call(context); + }); + + it('does not register mouseleave event if popover is not showed', () => { + spyOn(showPopover, 'call').and.returnValue(false); + const spy = spyOn($.fn, 'on').and.callFake(() => {}); + mouseenter.call(context); + expect(spy).not.toHaveBeenCalled(); + }); + }); + + describe('setupDismissButton', () => { + it('registers click event callback', (done) => { + const context = { + getAttribute: () => 'popoverId', + dataset: { + highlight: 'cookieId', + }, + }; + + spyOn($.fn, 'on').and.callFake((event) => { + expect(event).toEqual('click'); + done(); + }); + setupDismissButton.call(context); + }); + }); +}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js new file mode 100644 index 00000000000..7feb361edec --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js @@ -0,0 +1,45 @@ +import domContentLoaded from '~/feature_highlight/feature_highlight_options'; +import bp from '~/breakpoints'; + +describe('feature highlight options', () => { + describe('domContentLoaded', () => { + const highlightOrder = []; + + beforeEach(() => { + // Check for when highlightFeatures is called + spyOn(highlightOrder, 'find').and.callFake(() => {}); + }); + + it('should not call highlightFeatures when breakpoint is xs', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('xs'); + + domContentLoaded(highlightOrder); + expect(bp.getBreakpointSize).toHaveBeenCalled(); + expect(highlightOrder.find).not.toHaveBeenCalled(); + }); + + it('should not call highlightFeatures when breakpoint is sm', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); + + domContentLoaded(highlightOrder); + expect(bp.getBreakpointSize).toHaveBeenCalled(); + expect(highlightOrder.find).not.toHaveBeenCalled(); + }); + + it('should not call highlightFeatures when breakpoint is md', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('md'); + + domContentLoaded(highlightOrder); + expect(bp.getBreakpointSize).toHaveBeenCalled(); + expect(highlightOrder.find).not.toHaveBeenCalled(); + }); + + it('should call highlightFeatures when breakpoint is lg', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('lg'); + + domContentLoaded(highlightOrder); + expect(bp.getBreakpointSize).toHaveBeenCalled(); + expect(highlightOrder.find).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js new file mode 100644 index 00000000000..6abe8425ee7 --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js @@ -0,0 +1,122 @@ +import Cookies from 'js-cookie'; +import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; +import * as featureHighlight from '~/feature_highlight/feature_highlight'; + +describe('feature highlight', () => { + describe('setupFeatureHighlightPopover', () => { + const selector = '.js-feature-highlight[data-highlight=test]'; + beforeEach(() => { + setFixtures(` + <div> + <div class="js-feature-highlight" data-highlight="test" disabled> + Trigger + </div> + </div> + <div class="feature-highlight-popover-content"> + Content + <div class="dismiss-feature-highlight"> + Dismiss + </div> + </div> + `); + spyOn(window, 'addEventListener'); + spyOn(window, 'removeEventListener'); + featureHighlight.setupFeatureHighlightPopover('test', 0); + }); + + it('setups popover content', () => { + const $popoverContent = $('.feature-highlight-popover-content'); + const outerHTML = $popoverContent.prop('outerHTML'); + + expect($(selector).data('content')).toEqual(outerHTML); + }); + + it('setups mouseenter', () => { + const showSpy = spyOn(featureHighlightHelper.showPopover, 'call'); + $(selector).trigger('mouseenter'); + + expect(showSpy).toHaveBeenCalled(); + }); + + it('setups debounced mouseleave', (done) => { + const hideSpy = spyOn(featureHighlightHelper.hidePopover, 'call'); + $(selector).trigger('mouseleave'); + + // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce + setTimeout(() => { + expect(hideSpy).toHaveBeenCalled(); + done(); + }, 0); + }); + + it('setups inserted.bs.popover', () => { + $(selector).trigger('mouseenter'); + const popoverId = $(selector).attr('aria-describedby'); + const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click'); + + $(`#${popoverId} .dismiss-feature-highlight`).click(); + expect(spyEvent).toHaveBeenTriggered(); + }); + + it('setups show.bs.popover', () => { + $(selector).trigger('show.bs.popover'); + expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); + }); + + it('setups hide.bs.popover', () => { + $(selector).trigger('hide.bs.popover'); + expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); + }); + + it('removes disabled attribute', () => { + expect($('.js-feature-highlight').is(':disabled')).toEqual(false); + }); + + it('displays popover', () => { + expect($(selector).attr('aria-describedby')).toBeFalsy(); + $(selector).trigger('mouseenter'); + expect($(selector).attr('aria-describedby')).toBeTruthy(); + }); + }); + + describe('shouldHighlightFeature', () => { + it('should return false if element is not found', () => { + spyOn(document, 'querySelector').and.returnValue(null); + spyOn(Cookies, 'get').and.returnValue(null); + + expect(featureHighlight.shouldHighlightFeature()).toBeFalsy(); + }); + + it('should return false if previouslyDismissed', () => { + spyOn(document, 'querySelector').and.returnValue(document.createElement('div')); + spyOn(Cookies, 'get').and.returnValue('true'); + + expect(featureHighlight.shouldHighlightFeature()).toBeFalsy(); + }); + + it('should return true if element is found and not previouslyDismissed', () => { + spyOn(document, 'querySelector').and.returnValue(document.createElement('div')); + spyOn(Cookies, 'get').and.returnValue(null); + + expect(featureHighlight.shouldHighlightFeature()).toBeTruthy(); + }); + }); + + describe('highlightFeatures', () => { + it('calls setupFeatureHighlightPopover if shouldHighlightFeature returns true', () => { + // Mimic shouldHighlightFeature set to true + const highlightOrder = ['issue-boards']; + spyOn(highlightOrder, 'find').and.returnValue(highlightOrder[0]); + + expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(true); + }); + + it('does not call setupFeatureHighlightPopover if shouldHighlightFeature returns false', () => { + // Mimic shouldHighlightFeature set to false + const highlightOrder = ['issue-boards']; + spyOn(highlightOrder, 'find').and.returnValue(null); + + expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 10fcc590c89..dcb8dbce178 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -4,7 +4,10 @@ import '~/gl_dropdown'; import '~/lib/utils/common_utils'; import '~/lib/utils/url_utility'; -(() => { +describe('glDropdown', function describeDropdown() { + preloadFixtures('static/gl_dropdown.html.raw'); + loadJSONFixtures('projects.json'); + const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; const SEARCH_INPUT_SELECTOR = '.dropdown-input-field'; const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`; @@ -39,187 +42,217 @@ import '~/lib/utils/url_utility'; remoteCallback = callback.bind({}, data); }; - describe('Dropdown', function describeDropdown() { - preloadFixtures('static/gl_dropdown.html.raw'); - loadJSONFixtures('projects.json'); - - function initDropDown(hasRemote, isFilterable, extraOpts = {}) { - const options = Object.assign({ - selectable: true, - filterable: isFilterable, - data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, - search: { - fields: ['name'] - }, - text: project => (project.name_with_namespace || project.name), - id: project => project.id, - }, extraOpts); - this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options); - } + function initDropDown(hasRemote, isFilterable, extraOpts = {}) { + const options = Object.assign({ + selectable: true, + filterable: isFilterable, + data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, + search: { + fields: ['name'] + }, + text: project => (project.name_with_namespace || project.name), + id: project => project.id, + }, extraOpts); + this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options); + } + + beforeEach(() => { + loadFixtures('static/gl_dropdown.html.raw'); + this.dropdownContainerElement = $('.dropdown.inline'); + this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); + this.projectsData = getJSONFixture('projects.json'); + }); - beforeEach(() => { - loadFixtures('static/gl_dropdown.html.raw'); - this.dropdownContainerElement = $('.dropdown.inline'); - this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); - this.projectsData = getJSONFixture('projects.json'); - }); + afterEach(() => { + $('body').unbind('keydown'); + this.dropdownContainerElement.unbind('keyup'); + }); - afterEach(() => { - $('body').unbind('keydown'); - this.dropdownContainerElement.unbind('keyup'); - }); + it('should open on click', () => { + initDropDown.call(this, false); + expect(this.dropdownContainerElement).not.toHaveClass('open'); + this.dropdownButtonElement.click(); + expect(this.dropdownContainerElement).toHaveClass('open'); + }); - it('should open on click', () => { - initDropDown.call(this, false); - expect(this.dropdownContainerElement).not.toHaveClass('open'); - this.dropdownButtonElement.click(); - expect(this.dropdownContainerElement).toHaveClass('open'); - }); + it('escapes HTML as text', () => { + this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>'; - it('escapes HTML as text', () => { - this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>'; + initDropDown.call(this, false); - initDropDown.call(this, false); + this.dropdownButtonElement.click(); - this.dropdownButtonElement.click(); + expect( + $('.dropdown-content li:first-child').text(), + ).toBe('<script>alert("testing");</script>'); + }); - expect( - $('.dropdown-content li:first-child').text(), - ).toBe('<script>alert("testing");</script>'); - }); + it('should output HTML when highlighting', () => { + this.projectsData[0].name_with_namespace = 'testing'; + $('.dropdown-input .dropdown-input-field').val('test'); - it('should output HTML when highlighting', () => { - this.projectsData[0].name_with_namespace = 'testing'; - $('.dropdown-input .dropdown-input-field').val('test'); + initDropDown.call(this, false, true, { + highlight: true, + }); - initDropDown.call(this, false, true, { - highlight: true, - }); + this.dropdownButtonElement.click(); - this.dropdownButtonElement.click(); + expect( + $('.dropdown-content li:first-child').text(), + ).toBe('testing'); - expect( - $('.dropdown-content li:first-child').text(), - ).toBe('testing'); + expect( + $('.dropdown-content li:first-child a').html(), + ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing'); + }); - expect( - $('.dropdown-content li:first-child a').html(), - ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing'); + describe('that is open', () => { + beforeEach(() => { + initDropDown.call(this, false, false); + this.dropdownButtonElement.click(); }); - describe('that is open', () => { - beforeEach(() => { - initDropDown.call(this, false, false); - this.dropdownButtonElement.click(); + it('should select a following item on DOWN keypress', () => { + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); + const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0); + navigateWithKeys('down', randomIndex, () => { + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); + expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); }); + }); - it('should select a following item on DOWN keypress', () => { - expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); - const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0); - navigateWithKeys('down', randomIndex, () => { + it('should select a previous item on UP keypress', () => { + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); + navigateWithKeys('down', (this.projectsData.length - 1), () => { + expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); + const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0); + navigateWithKeys('up', randomIndex, () => { expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); - expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); + expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); }); }); + }); - it('should select a previous item on UP keypress', () => { - expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); - navigateWithKeys('down', (this.projectsData.length - 1), () => { - expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); - const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0); - navigateWithKeys('up', randomIndex, () => { - expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); - expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); - }); + it('should click the selected item on ENTER keypress', () => { + expect(this.dropdownContainerElement).toHaveClass('open'); + const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; + navigateWithKeys('down', randomIndex, () => { + spyOn(gl.utils, 'visitUrl').and.stub(); + navigateWithKeys('enter', null, () => { + expect(this.dropdownContainerElement).not.toHaveClass('open'); + const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); + expect(link).toHaveClass('is-active'); + const linkedLocation = link.attr('href'); + if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation); }); }); + }); - it('should click the selected item on ENTER keypress', () => { - expect(this.dropdownContainerElement).toHaveClass('open'); - const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; - navigateWithKeys('down', randomIndex, () => { - spyOn(gl.utils, 'visitUrl').and.stub(); - navigateWithKeys('enter', null, () => { - expect(this.dropdownContainerElement).not.toHaveClass('open'); - const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); - expect(link).toHaveClass('is-active'); - const linkedLocation = link.attr('href'); - if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation); - }); - }); + it('should close on ESC keypress', () => { + expect(this.dropdownContainerElement).toHaveClass('open'); + this.dropdownContainerElement.trigger({ + type: 'keyup', + which: ARROW_KEYS.ESC, + keyCode: ARROW_KEYS.ESC }); + expect(this.dropdownContainerElement).not.toHaveClass('open'); + }); + }); - it('should close on ESC keypress', () => { - expect(this.dropdownContainerElement).toHaveClass('open'); - this.dropdownContainerElement.trigger({ - type: 'keyup', - which: ARROW_KEYS.ESC, - keyCode: ARROW_KEYS.ESC - }); - expect(this.dropdownContainerElement).not.toHaveClass('open'); + describe('opened and waiting for a remote callback', () => { + beforeEach(() => { + initDropDown.call(this, true, true); + this.dropdownButtonElement.click(); + }); + + it('should show loading indicator while search results are being fetched by backend', () => { + const dropdownMenu = document.querySelector('.dropdown-menu'); + + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true); + remoteCallback(); + expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false); + }); + + it('should not focus search input while remote task is not complete', () => { + expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); + remoteCallback(); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + + it('should focus search input after remote task is complete', () => { + remoteCallback(); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + + it('should focus on input when opening for the second time after transition', () => { + remoteCallback(); + this.dropdownContainerElement.trigger({ + type: 'keyup', + which: ARROW_KEYS.ESC, + keyCode: ARROW_KEYS.ESC }); + this.dropdownButtonElement.click(); + this.dropdownContainerElement.trigger('transitionend'); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); + }); + + describe('input focus with array data', () => { + it('should focus input when passing array data to drop down', () => { + initDropDown.call(this, false, true); + this.dropdownButtonElement.click(); + this.dropdownContainerElement.trigger('transitionend'); + expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + }); + }); + + it('should still have input value on close and restore', () => { + const $searchInput = $(SEARCH_INPUT_SELECTOR); + initDropDown.call(this, false, true); + $searchInput + .trigger('focus') + .val('g') + .trigger('input'); + expect($searchInput.val()).toEqual('g'); + this.dropdownButtonElement.trigger('hidden.bs.dropdown'); + $searchInput + .trigger('blur') + .trigger('focus'); + expect($searchInput.val()).toEqual('g'); + }); + + describe('renderItem', () => { + describe('without selected value', () => { + let dropdown; - describe('opened and waiting for a remote callback', () => { beforeEach(() => { - initDropDown.call(this, true, true); - this.dropdownButtonElement.click(); + const dropdownOptions = { + + }; + const $dropdownDiv = $('<div />'); + $dropdownDiv.glDropdown(dropdownOptions); + dropdown = $dropdownDiv.data('glDropdown'); }); - it('should show loading indicator while search results are being fetched by backend', () => { - const dropdownMenu = document.querySelector('.dropdown-menu'); + it('marks items without ID as active', () => { + const dummyData = { }; - expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true); - remoteCallback(); - expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false); - }); + const html = dropdown.renderItem(dummyData, null, null); - it('should not focus search input while remote task is not complete', () => { - expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); - remoteCallback(); - expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + const link = html.querySelector('a'); + expect(link).toHaveClass('is-active'); }); - it('should focus search input after remote task is complete', () => { - remoteCallback(); - expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); - }); + it('does not mark items with ID as active', () => { + const dummyData = { + id: 'ea' + }; - it('should focus on input when opening for the second time after transition', () => { - remoteCallback(); - this.dropdownContainerElement.trigger({ - type: 'keyup', - which: ARROW_KEYS.ESC, - keyCode: ARROW_KEYS.ESC - }); - this.dropdownButtonElement.click(); - this.dropdownContainerElement.trigger('transitionend'); - expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); - }); - }); + const html = dropdown.renderItem(dummyData, null, null); - describe('input focus with array data', () => { - it('should focus input when passing array data to drop down', () => { - initDropDown.call(this, false, true); - this.dropdownButtonElement.click(); - this.dropdownContainerElement.trigger('transitionend'); - expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); + const link = html.querySelector('a'); + expect(link).not.toHaveClass('is-active'); }); }); - - it('should still have input value on close and restore', () => { - const $searchInput = $(SEARCH_INPUT_SELECTOR); - initDropDown.call(this, false, true); - $searchInput - .trigger('focus') - .val('g') - .trigger('input'); - expect($searchInput.val()).toEqual('g'); - this.dropdownButtonElement.trigger('hidden.bs.dropdown'); - $searchInput - .trigger('blur') - .trigger('focus'); - expect($searchInput.val()).toEqual('g'); - }); }); -})(); +}); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 3af26e2f28f..39065814bc2 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -34,7 +34,6 @@ describe('Issuable output', () => { propsData: { canUpdate: true, canDestroy: true, - canMove: true, endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes', issuableRef: '#1', initialTitleHtml: '', @@ -43,7 +42,6 @@ describe('Issuable output', () => { initialDescriptionText: '', markdownPreviewPath: '/', markdownDocsPath: '/', - projectsAutocompletePath: '/', isConfidential: false, projectNamespace: '/', projectPath: '/', @@ -226,7 +224,7 @@ describe('Issuable output', () => { }); }); - it('redirects if issue is moved', (done) => { + it('redirects if returned web_url has changed', (done) => { spyOn(gl.utils, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ @@ -250,23 +248,6 @@ describe('Issuable output', () => { }); }); - it('does not update issuable if project move confirm is false', (done) => { - spyOn(window, 'confirm').and.returnValue(false); - spyOn(vm.service, 'updateIssuable'); - - vm.store.formState.move_to_project_id = 1; - - vm.updateIssuable(); - - setTimeout(() => { - expect( - vm.service.updateIssuable, - ).not.toHaveBeenCalled(); - - done(); - }); - }); - it('closes form on error', (done) => { spyOn(window, 'Flash').and.callThrough(); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => { diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js deleted file mode 100644 index 8b6ed6a03a9..00000000000 --- a/spec/javascripts/issue_show/components/fields/project_move_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import Vue from 'vue'; -import projectMove from '~/issue_show/components/fields/project_move.vue'; - -describe('Project move field component', () => { - let vm; - let formState; - - beforeEach((done) => { - const Component = Vue.extend(projectMove); - - formState = { - move_to_project_id: 0, - }; - - vm = new Component({ - propsData: { - formState, - projectsAutocompletePath: '/autocomplete', - }, - }).$mount(); - - Vue.nextTick(done); - }); - - it('mounts select2 element', () => { - expect( - vm.$el.querySelector('.select2-container'), - ).not.toBeNull(); - }); - - it('updates formState on change', () => { - $(vm.$refs['move-dropdown']).val(2).trigger('change'); - - expect( - formState.move_to_project_id, - ).toBe(2); - }); -}); diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index d8af5287431..6e89528a3ea 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -12,7 +12,6 @@ describe('Inline edit form component', () => { vm = new Component({ propsData: { canDestroy: true, - canMove: true, formState: { title: 'b', description: 'a', @@ -20,7 +19,6 @@ describe('Inline edit form component', () => { }, markdownPreviewPath: '/', markdownDocsPath: '/', - projectsAutocompletePath: '/', projectPath: '/', projectNamespace: '/', }, diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index 731076a7d2a..14794cbfd50 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -32,10 +32,6 @@ describe('GraphFlag', () => { .toEqual(component.currentXCoordinate); expect(getCoordinate(component, '.selected-metric-line', 'x2')) .toEqual(component.currentXCoordinate); - expect(getCoordinate(component, '.circle-metric', 'cx')) - .toEqual(component.currentXCoordinate); - expect(getCoordinate(component, '.circle-metric', 'cy')) - .toEqual(component.currentYCoordinate); }); it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js index e877832dffd..da2fbd26e23 100644 --- a/spec/javascripts/monitoring/graph/legend_spec.js +++ b/spec/javascripts/monitoring/graph/legend_spec.js @@ -1,6 +1,8 @@ import Vue from 'vue'; import GraphLegend from '~/monitoring/components/graph/legend.vue'; import measurements from '~/monitoring/utils/measurements'; +import createTimeSeries from '~/monitoring/utils/multiple_time_series'; +import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; const createComponent = (propsData) => { const Component = Vue.extend(GraphLegend); @@ -10,6 +12,28 @@ const createComponent = (propsData) => { }).$mount(); }; +const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); + +const defaultValuesComponent = { + graphWidth: 500, + graphHeight: 300, + graphHeightOffset: 120, + margin: measurements.large.margin, + measurements: measurements.large, + areaColorRgb: '#f0f0f0', + legendTitle: 'Title', + yAxisLabel: 'Values', + metricUsage: 'Value', + unitOfDisplay: 'Req/Sec', + currentDataIndex: 0, +}; + +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, + defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight, + defaultValuesComponent.graphHeightOffset); + +defaultValuesComponent.timeSeries = timeSeries; + function getTextFromNode(component, selector) { return component.$el.querySelector(selector).firstChild.nodeValue.trim(); } @@ -17,95 +41,67 @@ function getTextFromNode(component, selector) { describe('GraphLegend', () => { describe('Computed props', () => { it('textTransform', () => { - const component = createComponent({ - graphWidth: 500, - graphHeight: 300, - margin: measurements.large.margin, - measurements: measurements.large, - areaColorRgb: '#f0f0f0', - legendTitle: 'Title', - yAxisLabel: 'Values', - metricUsage: 'Value', - }); + const component = createComponent(defaultValuesComponent); expect(component.textTransform).toContain('translate(15, 120) rotate(-90)'); }); it('xPosition', () => { - const component = createComponent({ - graphWidth: 500, - graphHeight: 300, - margin: measurements.large.margin, - measurements: measurements.large, - areaColorRgb: '#f0f0f0', - legendTitle: 'Title', - yAxisLabel: 'Values', - metricUsage: 'Value', - }); + const component = createComponent(defaultValuesComponent); expect(component.xPosition).toEqual(180); }); it('yPosition', () => { - const component = createComponent({ - graphWidth: 500, - graphHeight: 300, - margin: measurements.large.margin, - measurements: measurements.large, - areaColorRgb: '#f0f0f0', - legendTitle: 'Title', - yAxisLabel: 'Values', - metricUsage: 'Value', - }); + const component = createComponent(defaultValuesComponent); expect(component.yPosition).toEqual(240); }); it('rectTransform', () => { - const component = createComponent({ - graphWidth: 500, - graphHeight: 300, - margin: measurements.large.margin, - measurements: measurements.large, - areaColorRgb: '#f0f0f0', - legendTitle: 'Title', - yAxisLabel: 'Values', - metricUsage: 'Value', - }); + const component = createComponent(defaultValuesComponent); expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)'); }); }); - it('has 2 rect-axis-text rect svg elements', () => { - const component = createComponent({ - graphWidth: 500, - graphHeight: 300, - margin: measurements.large.margin, - measurements: measurements.large, - areaColorRgb: '#f0f0f0', - legendTitle: 'Title', - yAxisLabel: 'Values', - metricUsage: 'Value', + describe('methods', () => { + it('translateLegendGroup should only change Y direction', () => { + const component = createComponent(defaultValuesComponent); + + const translatedCoordinate = component.translateLegendGroup(1); + expect(translatedCoordinate.indexOf('translate(0, ')).not.toEqual(-1); }); + it('formatMetricUsage should contain the unit of display and the current value selected via "currentDataIndex"', () => { + const component = createComponent(defaultValuesComponent); + + const formattedMetricUsage = component.formatMetricUsage(timeSeries[0]); + const valueFromSeries = timeSeries[0].values[component.currentDataIndex].value; + expect(formattedMetricUsage.indexOf(component.unitOfDisplay)).not.toEqual(-1); + expect(formattedMetricUsage.indexOf(valueFromSeries)).not.toEqual(-1); + }); + }); + + it('has 2 rect-axis-text rect svg elements', () => { + const component = createComponent(defaultValuesComponent); + expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); }); it('contains text to signal the usage, title and time', () => { - const component = createComponent({ - graphWidth: 500, - graphHeight: 300, - margin: measurements.large.margin, - measurements: measurements.large, - areaColorRgb: '#f0f0f0', - legendTitle: 'Title', - yAxisLabel: 'Values', - metricUsage: 'Value', - }); + const component = createComponent(defaultValuesComponent); + const titles = component.$el.querySelectorAll('.legend-metric-title'); + + expect(getTextFromNode(component, '.legend-metric-title').indexOf(component.legendTitle)).not.toEqual(-1); + expect(titles[0].textContent.indexOf('Title')).not.toEqual(-1); + expect(titles[1].textContent.indexOf('Series')).not.toEqual(-1); + expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel); + }); + + it('should contain the same number of legend groups as the timeSeries length', () => { + const component = createComponent(defaultValuesComponent); - expect(getTextFromNode(component, '.text-metric-title')).toEqual(component.legendTitle); - expect(getTextFromNode(component, '.text-metric-usage')).toEqual(component.metricUsage); - expect(getTextFromNode(component, '.label-axis-text')).toEqual(component.yAxisLabel); + expect(component.$el.querySelectorAll('.legend-group').length).toEqual(component.timeSeries.length); }); }); diff --git a/spec/javascripts/monitoring/graph_row_spec.js b/spec/javascripts/monitoring/graph_row_spec.js deleted file mode 100644 index dd485473ccf..00000000000 --- a/spec/javascripts/monitoring/graph_row_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import Vue from 'vue'; -import GraphRow from '~/monitoring/components/graph_row.vue'; -import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; -import { deploymentData, singleRowMetrics } from './mock_data'; - -const createComponent = (propsData) => { - const Component = Vue.extend(GraphRow); - - return new Component({ - propsData, - }).$mount(); -}; - -describe('GraphRow', () => { - beforeEach(() => { - spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); - }); - - describe('Computed props', () => { - it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => { - const component = createComponent({ - rowData: singleRowMetrics, - updateAspectRatio: false, - deploymentData, - }); - - expect(component.bootstrapClass).toEqual('col-md-6'); - }); - - it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => { - const component = createComponent({ - rowData: [singleRowMetrics[0]], - updateAspectRatio: false, - deploymentData, - }); - - expect(component.bootstrapClass).toEqual('col-md-12'); - }); - }); - - it('has one column', () => { - const component = createComponent({ - rowData: singleRowMetrics, - updateAspectRatio: false, - deploymentData, - }); - - expect(component.$el.querySelectorAll('.prometheus-svg-container').length) - .toEqual(component.rowData.length); - }); - - it('has two columns', () => { - const component = createComponent({ - rowData: singleRowMetrics, - updateAspectRatio: false, - deploymentData, - }); - - expect(component.$el.querySelectorAll('.col-md-6').length) - .toEqual(component.rowData.length); - }); -}); diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index 6d6fe410113..7d8b0744af1 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -1,9 +1,8 @@ import Vue from 'vue'; -import _ from 'underscore'; import Graph from '~/monitoring/components/graph.vue'; import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import eventHub from '~/monitoring/event_hub'; -import { deploymentData, singleRowMetrics } from './mock_data'; +import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data'; const createComponent = (propsData) => { const Component = Vue.extend(Graph); @@ -13,6 +12,8 @@ const createComponent = (propsData) => { }).$mount(); }; +const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); + describe('Graph', () => { beforeEach(() => { spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); @@ -20,7 +21,7 @@ describe('Graph', () => { it('has a title', () => { const component = createComponent({ - graphData: singleRowMetrics[0], + graphData: convertedMetrics[1], classType: 'col-md-6', updateAspectRatio: false, deploymentData, @@ -29,29 +30,10 @@ describe('Graph', () => { expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title); }); - it('creates a path for the line and area of the graph', (done) => { - const component = createComponent({ - graphData: singleRowMetrics[0], - classType: 'col-md-6', - updateAspectRatio: false, - deploymentData, - }); - - Vue.nextTick(() => { - expect(component.area).toBeDefined(); - expect(component.line).toBeDefined(); - expect(typeof component.area).toEqual('string'); - expect(typeof component.line).toEqual('string'); - expect(_.isFunction(component.xScale)).toBe(true); - expect(_.isFunction(component.yScale)).toBe(true); - done(); - }); - }); - describe('Computed props', () => { it('axisTransform translates an element Y position depending of its height', () => { const component = createComponent({ - graphData: singleRowMetrics[0], + graphData: convertedMetrics[1], classType: 'col-md-6', updateAspectRatio: false, deploymentData, @@ -64,7 +46,7 @@ describe('Graph', () => { it('outterViewBox gets a width and height property based on the DOM size of the element', () => { const component = createComponent({ - graphData: singleRowMetrics[0], + graphData: convertedMetrics[1], classType: 'col-md-6', updateAspectRatio: false, deploymentData, @@ -79,7 +61,7 @@ describe('Graph', () => { it('sends an event to the eventhub when it has finished resizing', (done) => { const component = createComponent({ - graphData: singleRowMetrics[0], + graphData: convertedMetrics[1], classType: 'col-md-6', updateAspectRatio: false, deploymentData, @@ -95,7 +77,7 @@ describe('Graph', () => { it('has a title for the y-axis and the chart legend that comes from the backend', () => { const component = createComponent({ - graphData: singleRowMetrics[0], + graphData: convertedMetrics[1], classType: 'col-md-6', updateAspectRatio: false, deploymentData, diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index b69f4eddffc..3d399f2bb95 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -2473,1754 +2473,5848 @@ export const statePaths = { documentationPath: '/help/administration/monitoring/prometheus/index.md', }; -export const singleRowMetrics = [ - { - 'title': 'CPU usage', - 'weight': 1, - 'y_label': 'Memory', - 'queries': [ - { - 'query_range': 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', - 'label': 'Container CPU', - 'result': [ - { - 'metric': { - - }, - 'values': [ - { - 'time': '2017-06-04T21:22:59.508Z', - 'value': '0.06335544298150002' - }, - { - 'time': '2017-06-04T21:23:59.508Z', - 'value': '0.0420347312480917' - }, - { - 'time': '2017-06-04T21:24:59.508Z', - 'value': '0.0023175131665412706' - }, - { - 'time': '2017-06-04T21:25:59.508Z', - 'value': '0.002315870476190476' - }, - { - 'time': '2017-06-04T21:26:59.508Z', - 'value': '0.0025005961904761894' - }, - { - 'time': '2017-06-04T21:27:59.508Z', - 'value': '0.0024612605834341264' - }, - { - 'time': '2017-06-04T21:28:59.508Z', - 'value': '0.002313129398767631' - }, - { - 'time': '2017-06-04T21:29:59.508Z', - 'value': '0.002411067353663882' - }, - { - 'time': '2017-06-04T21:30:59.508Z', - 'value': '0.002577309263721303' - }, - { - 'time': '2017-06-04T21:31:59.508Z', - 'value': '0.00242688307730403' - }, - { - 'time': '2017-06-04T21:32:59.508Z', - 'value': '0.0024168360301330457' - }, - { - 'time': '2017-06-04T21:33:59.508Z', - 'value': '0.0020449528090743714' - }, - { - 'time': '2017-06-04T21:34:59.508Z', - 'value': '0.0019149619047619036' - }, - { - 'time': '2017-06-04T21:35:59.508Z', - 'value': '0.0024491714364625094' - }, - { - 'time': '2017-06-04T21:36:59.508Z', - 'value': '0.002728773131172677' - }, - { - 'time': '2017-06-04T21:37:59.508Z', - 'value': '0.0028439119047618997' - }, - { - 'time': '2017-06-04T21:38:59.508Z', - 'value': '0.0026307480952380917' - }, - { - 'time': '2017-06-04T21:39:59.508Z', - 'value': '0.0025024842620546446' - }, - { - 'time': '2017-06-04T21:40:59.508Z', - 'value': '0.002300662387260825' - }, - { - 'time': '2017-06-04T21:41:59.508Z', - 'value': '0.002052890924848337' - }, - { - 'time': '2017-06-04T21:42:59.508Z', - 'value': '0.0023711195238095275' - }, - { - 'time': '2017-06-04T21:43:59.508Z', - 'value': '0.002513477619047618' - }, - { - 'time': '2017-06-04T21:44:59.508Z', - 'value': '0.0023489776287844897' - }, - { - 'time': '2017-06-04T21:45:59.508Z', - 'value': '0.002542572310212481' - }, - { - 'time': '2017-06-04T21:46:59.508Z', - 'value': '0.0024579470671707952' - }, - { - 'time': '2017-06-04T21:47:59.508Z', - 'value': '0.0028725150236664403' - }, - { - 'time': '2017-06-04T21:48:59.508Z', - 'value': '0.0024356089105610525' - }, - { - 'time': '2017-06-04T21:49:59.508Z', - 'value': '0.002544015828269929' - }, - { - 'time': '2017-06-04T21:50:59.508Z', - 'value': '0.0029595013380824906' - }, - { - 'time': '2017-06-04T21:51:59.508Z', - 'value': '0.0023084015085858' - }, - { - 'time': '2017-06-04T21:52:59.508Z', - 'value': '0.0021070500000000083' - }, - { - 'time': '2017-06-04T21:53:59.508Z', - 'value': '0.0022950066191106617' - }, - { - 'time': '2017-06-04T21:54:59.508Z', - 'value': '0.002492719454470995' - }, - { - 'time': '2017-06-04T21:55:59.508Z', - 'value': '0.00244312761904762' - }, - { - 'time': '2017-06-04T21:56:59.508Z', - 'value': '0.0023495500000000028' - }, - { - 'time': '2017-06-04T21:57:59.508Z', - 'value': '0.0020597072353070005' - }, - { - 'time': '2017-06-04T21:58:59.508Z', - 'value': '0.0021482352044800866' - }, - { - 'time': '2017-06-04T21:59:59.508Z', - 'value': '0.002333490000000004' - }, - { - 'time': '2017-06-04T22:00:59.508Z', - 'value': '0.0025899442857142815' - }, - { - 'time': '2017-06-04T22:01:59.508Z', - 'value': '0.002430299999999999' - }, - { - 'time': '2017-06-04T22:02:59.508Z', - 'value': '0.0023550328092113476' - }, - { - 'time': '2017-06-04T22:03:59.508Z', - 'value': '0.0026521871636872793' - }, - { - 'time': '2017-06-04T22:04:59.508Z', - 'value': '0.0023080671428571398' - }, - { - 'time': '2017-06-04T22:05:59.508Z', - 'value': '0.0024108401032390896' - }, - { - 'time': '2017-06-04T22:06:59.508Z', - 'value': '0.002433249366678738' - }, - { - 'time': '2017-06-04T22:07:59.508Z', - 'value': '0.0023242202306688682' - }, - { - 'time': '2017-06-04T22:08:59.508Z', - 'value': '0.002388222857142859' - }, - { - 'time': '2017-06-04T22:09:59.508Z', - 'value': '0.002115974914046794' - }, - { - 'time': '2017-06-04T22:10:59.508Z', - 'value': '0.0025090043331269917' - }, - { - 'time': '2017-06-04T22:11:59.508Z', - 'value': '0.002445507057277277' - }, - { - 'time': '2017-06-04T22:12:59.508Z', - 'value': '0.0026348773751130976' - }, - { - 'time': '2017-06-04T22:13:59.508Z', - 'value': '0.0025616258583088104' - }, - { - 'time': '2017-06-04T22:14:59.508Z', - 'value': '0.0021544093415751505' - }, - { - 'time': '2017-06-04T22:15:59.508Z', - 'value': '0.002649394767668881' - }, - { - 'time': '2017-06-04T22:16:59.508Z', - 'value': '0.0024023332666685705' - }, - { - 'time': '2017-06-04T22:17:59.508Z', - 'value': '0.0025444105294235306' - }, - { - 'time': '2017-06-04T22:18:59.508Z', - 'value': '0.0027298872305772806' - }, - { - 'time': '2017-06-04T22:19:59.508Z', - 'value': '0.0022880104956379287' - }, - { - 'time': '2017-06-04T22:20:59.508Z', - 'value': '0.002473246666666661' - }, - { - 'time': '2017-06-04T22:21:59.508Z', - 'value': '0.002259948381935587' - }, - { - 'time': '2017-06-04T22:22:59.508Z', - 'value': '0.0025778470886268835' - }, - { - 'time': '2017-06-04T22:23:59.508Z', - 'value': '0.002246127910852894' - }, - { - 'time': '2017-06-04T22:24:59.508Z', - 'value': '0.0020697466666666758' - }, - { - 'time': '2017-06-04T22:25:59.508Z', - 'value': '0.00225859722473547' - }, - { - 'time': '2017-06-04T22:26:59.508Z', - 'value': '0.0026466728254554814' - }, - { - 'time': '2017-06-04T22:27:59.508Z', - 'value': '0.002151247619047619' - }, - { - 'time': '2017-06-04T22:28:59.508Z', - 'value': '0.002324161444543914' - }, - { - 'time': '2017-06-04T22:29:59.508Z', - 'value': '0.002476474313796452' - }, - { - 'time': '2017-06-04T22:30:59.508Z', - 'value': '0.0023922184232080517' - }, - { - 'time': '2017-06-04T22:31:59.508Z', - 'value': '0.0025094934237468933' - }, - { - 'time': '2017-06-04T22:32:59.508Z', - 'value': '0.0025665311098200883' - }, - { - 'time': '2017-06-04T22:33:59.508Z', - 'value': '0.0024154900681661374' - }, - { - 'time': '2017-06-04T22:34:59.508Z', - 'value': '0.0023267450166192037' - }, - { - 'time': '2017-06-04T22:35:59.508Z', - 'value': '0.002156521904761904' - }, - { - 'time': '2017-06-04T22:36:59.508Z', - 'value': '0.0025474356898637007' - }, - { - 'time': '2017-06-04T22:37:59.508Z', - 'value': '0.0025989409624670233' - }, - { - 'time': '2017-06-04T22:38:59.508Z', - 'value': '0.002348336664762987' - }, - { - 'time': '2017-06-04T22:39:59.508Z', - 'value': '0.002665888246554726' - }, - { - 'time': '2017-06-04T22:40:59.508Z', - 'value': '0.002652684787474174' - }, - { - 'time': '2017-06-04T22:41:59.508Z', - 'value': '0.002472620430865355' - }, - { - 'time': '2017-06-04T22:42:59.508Z', - 'value': '0.0020616469210110247' - }, - { - 'time': '2017-06-04T22:43:59.508Z', - 'value': '0.0022434546372311934' - }, - { - 'time': '2017-06-04T22:44:59.508Z', - 'value': '0.0024469386784827982' - }, - { - 'time': '2017-06-04T22:45:59.508Z', - 'value': '0.0026192823809523787' - }, - { - 'time': '2017-06-04T22:46:59.508Z', - 'value': '0.003451999542852798' - }, - { - 'time': '2017-06-04T22:47:59.508Z', - 'value': '0.0031780314285714288' - }, - { - 'time': '2017-06-04T22:48:59.508Z', - 'value': '0.0024403352380952415' - }, - { - 'time': '2017-06-04T22:49:59.508Z', - 'value': '0.001998824761904764' - }, - { - 'time': '2017-06-04T22:50:59.508Z', - 'value': '0.0023792404761904806' - }, - { - 'time': '2017-06-04T22:51:59.508Z', - 'value': '0.002725906190476185' - }, - { - 'time': '2017-06-04T22:52:59.508Z', - 'value': '0.0020989528671155624' - }, - { - 'time': '2017-06-04T22:53:59.508Z', - 'value': '0.00228808226745016' - }, - { - 'time': '2017-06-04T22:54:59.508Z', - 'value': '0.0019860807413192147' - }, - { - 'time': '2017-06-04T22:55:59.508Z', - 'value': '0.0022698085714285897' - }, - { - 'time': '2017-06-04T22:56:59.508Z', - 'value': '0.0022839098467604415' - }, - { - 'time': '2017-06-04T22:57:59.508Z', - 'value': '0.002531114761904749' - }, - { - 'time': '2017-06-04T22:58:59.508Z', - 'value': '0.0028941072550999016' - }, - { - 'time': '2017-06-04T22:59:59.508Z', - 'value': '0.002547169523809506' - }, - { - 'time': '2017-06-04T23:00:59.508Z', - 'value': '0.0024062999999999958' - }, - { - 'time': '2017-06-04T23:01:59.508Z', - 'value': '0.0026939518471604386' - }, - { - 'time': '2017-06-04T23:02:59.508Z', - 'value': '0.002362901428571429' - }, - { - 'time': '2017-06-04T23:03:59.508Z', - 'value': '0.002663927142857154' - }, - { - 'time': '2017-06-04T23:04:59.508Z', - 'value': '0.0026173314285714354' - }, - { - 'time': '2017-06-04T23:05:59.508Z', - 'value': '0.002326527366406044' - }, - { - 'time': '2017-06-04T23:06:59.508Z', - 'value': '0.002035313809523809' - }, - { - 'time': '2017-06-04T23:07:59.508Z', - 'value': '0.002421447414786533' - }, - { - 'time': '2017-06-04T23:08:59.508Z', - 'value': '0.002898313809523804' - }, - { - 'time': '2017-06-04T23:09:59.508Z', - 'value': '0.002544891856112907' - }, - { - 'time': '2017-06-04T23:10:59.508Z', - 'value': '0.002290625356938882' - }, - { - 'time': '2017-06-04T23:11:59.508Z', - 'value': '0.002483028095238096' - }, - { - 'time': '2017-06-04T23:12:59.508Z', - 'value': '0.0023396832350784237' - }, - { - 'time': '2017-06-04T23:13:59.508Z', - 'value': '0.002085529248176153' - }, - { - 'time': '2017-06-04T23:14:59.508Z', - 'value': '0.0022417815068428012' - }, - { - 'time': '2017-06-04T23:15:59.508Z', - 'value': '0.002660293333333341' - }, - { - 'time': '2017-06-04T23:16:59.508Z', - 'value': '0.0029845149093818226' - }, - { - 'time': '2017-06-04T23:17:59.508Z', - 'value': '0.0027716655079475464' - }, - { - 'time': '2017-06-04T23:18:59.508Z', - 'value': '0.0025217708908741128' - }, - { - 'time': '2017-06-04T23:19:59.508Z', - 'value': '0.0025811235131094055' - }, - { - 'time': '2017-06-04T23:20:59.508Z', - 'value': '0.002209904761904762' - }, - { - 'time': '2017-06-04T23:21:59.508Z', - 'value': '0.0025053322926383344' - }, - { - 'time': '2017-06-04T23:22:59.508Z', - 'value': '0.002350917636526411' - }, - { - 'time': '2017-06-04T23:23:59.508Z', - 'value': '0.0018477500000000078' - }, - { - 'time': '2017-06-04T23:24:59.508Z', - 'value': '0.002427629523809527' - }, - { - 'time': '2017-06-04T23:25:59.508Z', - 'value': '0.0019305498147601655' - }, - { - 'time': '2017-06-04T23:26:59.508Z', - 'value': '0.002097250000000006' - }, - { - 'time': '2017-06-04T23:27:59.508Z', - 'value': '0.002675020952780041' - }, - { - 'time': '2017-06-04T23:28:59.508Z', - 'value': '0.0023142214285714374' - }, - { - 'time': '2017-06-04T23:29:59.508Z', - 'value': '0.0023644723809523737' - }, - { - 'time': '2017-06-04T23:30:59.508Z', - 'value': '0.002108696190476198' - }, - { - 'time': '2017-06-04T23:31:59.508Z', - 'value': '0.0019918289697997194' - }, - { - 'time': '2017-06-04T23:32:59.508Z', - 'value': '0.001583584285714283' - }, - { - 'time': '2017-06-04T23:33:59.508Z', - 'value': '0.002073770226383112' - }, - { - 'time': '2017-06-04T23:34:59.508Z', - 'value': '0.0025877664234966818' - }, - { - 'time': '2017-06-04T23:35:59.508Z', - 'value': '0.0021138238095238147' - }, - { - 'time': '2017-06-04T23:36:59.508Z', - 'value': '0.0022140838095238303' - }, - { - 'time': '2017-06-04T23:37:59.508Z', - 'value': '0.0018592674425248847' - }, - { - 'time': '2017-06-04T23:38:59.508Z', - 'value': '0.0020461969533657016' - }, - { - 'time': '2017-06-04T23:39:59.508Z', - 'value': '0.0021593628571428543' - }, - { - 'time': '2017-06-04T23:40:59.508Z', - 'value': '0.0024330682564928188' - }, - { - 'time': '2017-06-04T23:41:59.508Z', - 'value': '0.0021501804779093174' - }, - { - 'time': '2017-06-04T23:42:59.508Z', - 'value': '0.0025787493928397945' - }, - { - 'time': '2017-06-04T23:43:59.508Z', - 'value': '0.002593657082448396' - }, - { - 'time': '2017-06-04T23:44:59.508Z', - 'value': '0.0021316752380952306' - }, - { - 'time': '2017-06-04T23:45:59.508Z', - 'value': '0.0026972905019952086' - }, - { - 'time': '2017-06-04T23:46:59.508Z', - 'value': '0.002580250764292983' - }, - { - 'time': '2017-06-04T23:47:59.508Z', - 'value': '0.00227103000000001' - }, - { - 'time': '2017-06-04T23:48:59.508Z', - 'value': '0.0023678515647321146' - }, - { - 'time': '2017-06-04T23:49:59.508Z', - 'value': '0.002371472857142866' - }, - { - 'time': '2017-06-04T23:50:59.508Z', - 'value': '0.0026181353688500978' - }, - { - 'time': '2017-06-04T23:51:59.508Z', - 'value': '0.0025609667711121217' - }, - { - 'time': '2017-06-04T23:52:59.508Z', - 'value': '0.0027145308139922557' - }, - { - 'time': '2017-06-04T23:53:59.508Z', - 'value': '0.0024249397613310512' - }, - { - 'time': '2017-06-04T23:54:59.508Z', - 'value': '0.002399907142857147' - }, - { - 'time': '2017-06-04T23:55:59.508Z', - 'value': '0.0024753357142857195' - }, - { - 'time': '2017-06-04T23:56:59.508Z', - 'value': '0.0026179149325231575' - }, - { - 'time': '2017-06-04T23:57:59.508Z', - 'value': '0.0024261340368186956' - }, - { - 'time': '2017-06-04T23:58:59.508Z', - 'value': '0.0021061071428571517' - }, - { - 'time': '2017-06-04T23:59:59.508Z', - 'value': '0.0024033971105037015' - }, - { - 'time': '2017-06-05T00:00:59.508Z', - 'value': '0.0028287676190475956' - }, - { - 'time': '2017-06-05T00:01:59.508Z', - 'value': '0.002499719050294778' - }, - { - 'time': '2017-06-05T00:02:59.508Z', - 'value': '0.0026726102153353856' - }, - { - 'time': '2017-06-05T00:03:59.508Z', - 'value': '0.00262582619047618' - }, - { - 'time': '2017-06-05T00:04:59.508Z', - 'value': '0.002280473147363316' - }, - { - 'time': '2017-06-05T00:05:59.508Z', - 'value': '0.002095581470652675' - }, - { - 'time': '2017-06-05T00:06:59.508Z', - 'value': '0.002270768490828408' - }, - { - 'time': '2017-06-05T00:07:59.508Z', - 'value': '0.002728577415023017' - }, - { - 'time': '2017-06-05T00:08:59.508Z', - 'value': '0.002652512857142863' - }, - { - 'time': '2017-06-05T00:09:59.508Z', - 'value': '0.0022781033924455674' - }, - { - 'time': '2017-06-05T00:10:59.508Z', - 'value': '0.0025345038095238234' - }, - { - 'time': '2017-06-05T00:11:59.508Z', - 'value': '0.002376050020000397' - }, - { - 'time': '2017-06-05T00:12:59.508Z', - 'value': '0.002455068143506122' - }, - { - 'time': '2017-06-05T00:13:59.508Z', - 'value': '0.002826705714285719' - }, - { - 'time': '2017-06-05T00:14:59.508Z', - 'value': '0.002343833692070314' - }, - { - 'time': '2017-06-05T00:15:59.508Z', - 'value': '0.00264853297122164' - }, - { - 'time': '2017-06-05T00:16:59.508Z', - 'value': '0.0027656335117426257' - }, - { - 'time': '2017-06-05T00:17:59.508Z', - 'value': '0.0025896543842439564' - }, - { - 'time': '2017-06-05T00:18:59.508Z', - 'value': '0.002180053237081201' - }, - { - 'time': '2017-06-05T00:19:59.508Z', - 'value': '0.002475245002333342' - }, - { - 'time': '2017-06-05T00:20:59.508Z', - 'value': '0.0027559767805101065' - }, - { - 'time': '2017-06-05T00:21:59.508Z', - 'value': '0.0022294836141296607' - }, - { - 'time': '2017-06-05T00:22:59.508Z', - 'value': '0.0021383590476190643' - }, - { - 'time': '2017-06-05T00:23:59.508Z', - 'value': '0.002085417956361494' - }, - { - 'time': '2017-06-05T00:24:59.508Z', - 'value': '0.0024140319047619013' - }, - { - 'time': '2017-06-05T00:25:59.508Z', - 'value': '0.0024513114285714304' - }, - { - 'time': '2017-06-05T00:26:59.508Z', - 'value': '0.0026932152380952446' - }, - { - 'time': '2017-06-05T00:27:59.508Z', - 'value': '0.0022656844350898517' - }, - { - 'time': '2017-06-05T00:28:59.508Z', - 'value': '0.0024483785714285704' - }, - { - 'time': '2017-06-05T00:29:59.508Z', - 'value': '0.002559505804817207' - }, - { - 'time': '2017-06-05T00:30:59.508Z', - 'value': '0.0019485681088751649' - }, - { - 'time': '2017-06-05T00:31:59.508Z', - 'value': '0.00228367984456996' - }, - { - 'time': '2017-06-05T00:32:59.508Z', - 'value': '0.002522149047619049' - }, - { - 'time': '2017-06-05T00:33:59.508Z', - 'value': '0.0026860117715406737' - }, - { - 'time': '2017-06-05T00:34:59.508Z', - 'value': '0.002679669523809523' - }, - { - 'time': '2017-06-05T00:35:59.508Z', - 'value': '0.0022201920970675937' - }, - { - 'time': '2017-06-05T00:36:59.508Z', - 'value': '0.0022917647619047615' - }, - { - 'time': '2017-06-05T00:37:59.508Z', - 'value': '0.0021774059294673576' - }, - { - 'time': '2017-06-05T00:38:59.508Z', - 'value': '0.0024637766666666763' - }, - { - 'time': '2017-06-05T00:39:59.508Z', - 'value': '0.002470468290174195' - }, - { - 'time': '2017-06-05T00:40:59.508Z', - 'value': '0.0022188616082057812' - }, - { - 'time': '2017-06-05T00:41:59.508Z', - 'value': '0.002421840744373875' - }, - { - 'time': '2017-06-05T00:42:59.508Z', - 'value': '0.0023918266666666547' - }, - { - 'time': '2017-06-05T00:43:59.508Z', - 'value': '0.002195743809523809' - }, - { - 'time': '2017-06-05T00:44:59.508Z', - 'value': '0.0025514828571428687' - }, - { - 'time': '2017-06-05T00:45:59.508Z', - 'value': '0.0027981709349612694' - }, - { - 'time': '2017-06-05T00:46:59.508Z', - 'value': '0.002557977142857146' - }, - { - 'time': '2017-06-05T00:47:59.508Z', - 'value': '0.002213244285714286' - }, - { - 'time': '2017-06-05T00:48:59.508Z', - 'value': '0.0025706738095238046' - }, - { - 'time': '2017-06-05T00:49:59.508Z', - 'value': '0.002210976666666671' - }, - { - 'time': '2017-06-05T00:50:59.508Z', - 'value': '0.002055377091646749' - }, - { - 'time': '2017-06-05T00:51:59.508Z', - 'value': '0.002308368095238119' - }, - { - 'time': '2017-06-05T00:52:59.508Z', - 'value': '0.0024687939885141615' - }, - { - 'time': '2017-06-05T00:53:59.508Z', - 'value': '0.002563018571428578' - }, - { - 'time': '2017-06-05T00:54:59.508Z', - 'value': '0.00240563291078959' - } - ] - } +export const singleRowMetricsMultipleSeries = [ + { + 'title': 'Multiple Time Series', + 'weight': 1, + 'y_label': 'Request Rates', + 'queries': [ + { + 'query_range': 'sum(rate(nginx_responses_total{environment="production"}[2m])) by (status_code)', + 'label': 'Requests', + 'unit': 'Req/sec', + 'result': [ + { + 'metric': { + 'status_code': '1xx' + }, + 'values': [ + { + 'time': '2017-08-27T11:01:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:02:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:03:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:04:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:05:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:06:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:07:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:08:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:09:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:10:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:11:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:12:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:13:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:14:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:15:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:16:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:17:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:18:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:19:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:20:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:21:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:22:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:23:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:24:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:25:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:26:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:27:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:28:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:29:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:30:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:31:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:32:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:33:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:34:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:35:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:36:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:37:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:38:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:39:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:40:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:41:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:42:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:43:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:44:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:45:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:46:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:47:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:48:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:49:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:50:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:51:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:52:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:53:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:54:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:55:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:56:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:57:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:58:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T11:59:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:00:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:01:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:02:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:03:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:04:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:05:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:06:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:07:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:08:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:09:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:10:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:11:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:12:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:13:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:14:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:15:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:16:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:17:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:18:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:19:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:20:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:21:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:22:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:23:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:24:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:25:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:26:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:27:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:28:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:29:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:30:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:31:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:32:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:33:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:34:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:35:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:36:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:37:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:38:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:39:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:40:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:41:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:42:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:43:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:44:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:45:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:46:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:47:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:48:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:49:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:50:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:51:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:52:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:53:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:54:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:55:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:56:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:57:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:58:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T12:59:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:00:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:01:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:02:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:03:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:04:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:05:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:06:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:07:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:08:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:09:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:10:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:11:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:12:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:13:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:14:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:15:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:16:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:17:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:18:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:19:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:20:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:21:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:22:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:23:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:24:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:25:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:26:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:27:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:28:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:29:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:30:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:31:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:32:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:33:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:34:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:35:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:36:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:37:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:38:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:39:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:40:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:41:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:42:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:43:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:44:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:45:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:46:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:47:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:48:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:49:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:50:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:51:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:52:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:53:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:54:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:55:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:56:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:57:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:58:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T13:59:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:00:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:01:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:02:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:03:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:04:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:05:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:06:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:07:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:08:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:09:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:10:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:11:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:12:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:13:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:14:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:15:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:16:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:17:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:18:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:19:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:20:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:21:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:22:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:23:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:24:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:25:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:26:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:27:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:28:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:29:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:30:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:31:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:32:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:33:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:34:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:35:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:36:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:37:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:38:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:39:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:40:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:41:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:42:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:43:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:44:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:45:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:46:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:47:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:48:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:49:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:50:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:51:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:52:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:53:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:54:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:55:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:56:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:57:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:58:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T14:59:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:00:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:01:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:02:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:03:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:04:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:05:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:06:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:07:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:08:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:09:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:10:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:11:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:12:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:13:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:14:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:15:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:16:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:17:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:18:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:19:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:20:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:21:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:22:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:23:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:24:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:25:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:26:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:27:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:28:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:29:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:30:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:31:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:32:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:33:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:34:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:35:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:36:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:37:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:38:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:39:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:40:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:41:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:42:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:43:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:44:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:45:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:46:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:47:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:48:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:49:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:50:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:51:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:52:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:53:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:54:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:55:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:56:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:57:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:58:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T15:59:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:00:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:01:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:02:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:03:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:04:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:05:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:06:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:07:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:08:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:09:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:10:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:11:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:12:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:13:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:14:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:15:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:16:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:17:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:18:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:19:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:20:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:21:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:22:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:23:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:24:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:25:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:26:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:27:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:28:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:29:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:30:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:31:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:32:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:33:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:34:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:35:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:36:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:37:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:38:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:39:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:40:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:41:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:42:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:43:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:44:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:45:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:46:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:47:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:48:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:49:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:50:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:51:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:52:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:53:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:54:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:55:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:56:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:57:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:58:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T16:59:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:00:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:01:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:02:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:03:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:04:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:05:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:06:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:07:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:08:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:09:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:10:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:11:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:12:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:13:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:14:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:15:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:16:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:17:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:18:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:19:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:20:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:21:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:22:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:23:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:24:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:25:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:26:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:27:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:28:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:29:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:30:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:31:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:32:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:33:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:34:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:35:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:36:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:37:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:38:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:39:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:40:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:41:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:42:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:43:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:44:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:45:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:46:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:47:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:48:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:49:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:50:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:51:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:52:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:53:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:54:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:55:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:56:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:57:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:58:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T17:59:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:00:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:01:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:02:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:03:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:04:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:05:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:06:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:07:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:08:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:09:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:10:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:11:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:12:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:13:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:14:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:15:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:16:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:17:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:18:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:19:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:20:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:21:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:22:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:23:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:24:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:25:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:26:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:27:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:28:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:29:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:30:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:31:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:32:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:33:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:34:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:35:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:36:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:37:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:38:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:39:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:40:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:41:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:42:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:43:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:44:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:45:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:46:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:47:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:48:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:49:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:50:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:51:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:52:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:53:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:54:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:55:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:56:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:57:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:58:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T18:59:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T19:00:51.462Z', + 'value': '0' + }, + { + 'time': '2017-08-27T19:01:51.462Z', + 'value': '0' + } + ] + }, + { + 'metric': { + 'status_code': '2xx' + }, + 'values': [ + { + 'time': '2017-08-27T11:01:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:02:51.462Z', + 'value': '1.2571428571428571' + }, + { + 'time': '2017-08-27T11:03:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:04:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:05:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:06:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:07:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:08:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:09:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:10:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:11:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:12:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:13:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:14:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:15:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:16:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:17:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:18:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:19:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:20:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:21:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:22:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:23:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:24:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:25:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:26:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:27:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:28:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:29:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:30:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:31:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:32:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:33:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:34:51.462Z', + 'value': '1.333320635041571' + }, + { + 'time': '2017-08-27T11:35:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:36:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:37:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:38:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:39:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:40:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:41:51.462Z', + 'value': '1.3333587306424883' + }, + { + 'time': '2017-08-27T11:42:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:43:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:44:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:45:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:46:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:47:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:48:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:49:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T11:50:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:51:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:52:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:53:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:54:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:55:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:56:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:57:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T11:58:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T11:59:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:00:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:01:51.462Z', + 'value': '1.3333460318669703' + }, + { + 'time': '2017-08-27T12:02:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:03:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:04:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:05:51.462Z', + 'value': '1.31427319739812' + }, + { + 'time': '2017-08-27T12:06:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:07:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:08:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:09:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:10:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:11:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:12:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:13:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:14:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:15:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:16:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:17:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:18:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:19:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:20:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:21:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:22:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:23:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:24:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:25:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:26:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:27:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:28:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:29:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:30:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:31:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:32:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:33:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:34:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:35:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:36:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:37:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:38:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:39:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:40:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:41:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:42:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:43:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:44:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:45:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:46:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:47:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:48:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:49:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:50:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:51:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:52:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:53:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:54:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T12:55:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:56:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:57:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T12:58:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T12:59:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:00:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:01:51.462Z', + 'value': '1.295225759754669' + }, + { + 'time': '2017-08-27T13:02:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:03:51.462Z', + 'value': '1.2952627669098458' + }, + { + 'time': '2017-08-27T13:04:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:05:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:06:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:07:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:08:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:09:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:10:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:11:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:12:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:13:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:14:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:15:51.462Z', + 'value': '1.2571428571428571' + }, + { + 'time': '2017-08-27T13:16:51.462Z', + 'value': '1.3333587306424883' + }, + { + 'time': '2017-08-27T13:17:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:18:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:19:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:20:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:21:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:22:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:23:51.462Z', + 'value': '1.276190476190476' + }, + { + 'time': '2017-08-27T13:24:51.462Z', + 'value': '1.2571428571428571' + }, + { + 'time': '2017-08-27T13:25:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:26:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:27:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:28:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:29:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:30:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:31:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:32:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:33:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:34:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:35:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:36:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:37:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:38:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:39:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:40:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:41:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:42:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:43:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:44:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:45:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:46:51.462Z', + 'value': '1.2571428571428571' + }, + { + 'time': '2017-08-27T13:47:51.462Z', + 'value': '1.276190476190476' + }, + { + 'time': '2017-08-27T13:48:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:49:51.462Z', + 'value': '1.295225759754669' + }, + { + 'time': '2017-08-27T13:50:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:51:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:52:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:53:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:54:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:55:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:56:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T13:57:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T13:58:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T13:59:51.462Z', + 'value': '1.295225759754669' + }, + { + 'time': '2017-08-27T14:00:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:01:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:02:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:03:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:04:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:05:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:06:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:07:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:08:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:09:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:10:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:11:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:12:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:13:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:14:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:15:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:16:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:17:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:18:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:19:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:20:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:21:51.462Z', + 'value': '1.3333079369916765' + }, + { + 'time': '2017-08-27T14:22:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:23:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:24:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:25:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:26:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:27:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:28:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:29:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:30:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:31:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:32:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:33:51.462Z', + 'value': '1.2571428571428571' + }, + { + 'time': '2017-08-27T14:34:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:35:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:36:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:37:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:38:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:39:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:40:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:41:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:42:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:43:51.462Z', + 'value': '1.276190476190476' + }, + { + 'time': '2017-08-27T14:44:51.462Z', + 'value': '1.2571428571428571' + }, + { + 'time': '2017-08-27T14:45:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:46:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:47:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:48:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:49:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:50:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:51:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:52:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:53:51.462Z', + 'value': '1.333320635041571' + }, + { + 'time': '2017-08-27T14:54:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:55:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T14:56:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:57:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T14:58:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T14:59:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T15:00:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:01:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:02:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:03:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:04:51.462Z', + 'value': '1.2571428571428571' + }, + { + 'time': '2017-08-27T15:05:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:06:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:07:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:08:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:09:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:10:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:11:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:12:51.462Z', + 'value': '1.31427319739812' + }, + { + 'time': '2017-08-27T15:13:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:14:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:15:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:16:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T15:17:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:18:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:19:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:20:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T15:21:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:22:51.462Z', + 'value': '1.3333460318669703' + }, + { + 'time': '2017-08-27T15:23:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:24:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:25:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:26:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:27:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:28:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:29:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:30:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:31:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T15:32:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:33:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T15:34:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:35:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T15:36:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:37:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:38:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T15:39:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:40:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:41:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:42:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:43:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:44:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:45:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:46:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:47:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:48:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:49:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T15:50:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:51:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:52:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:53:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:54:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:55:51.462Z', + 'value': '1.3333587306424883' + }, + { + 'time': '2017-08-27T15:56:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T15:57:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:58:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T15:59:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:00:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:01:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:02:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:03:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:04:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:05:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:06:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:07:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:08:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:09:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:10:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:11:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:12:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:13:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:14:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:15:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:16:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:17:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:18:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:19:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:20:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:21:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:22:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:23:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:24:51.462Z', + 'value': '1.295225759754669' + }, + { + 'time': '2017-08-27T16:25:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:26:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:27:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:28:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:29:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:30:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:31:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:32:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:33:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:34:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:35:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:36:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:37:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:38:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:39:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:40:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:41:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:42:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:43:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:44:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:45:51.462Z', + 'value': '1.3142982314117277' + }, + { + 'time': '2017-08-27T16:46:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:47:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:48:51.462Z', + 'value': '1.333320635041571' + }, + { + 'time': '2017-08-27T16:49:51.462Z', + 'value': '1.31427319739812' + }, + { + 'time': '2017-08-27T16:50:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:51:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:52:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:53:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:54:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:55:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T16:56:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:57:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T16:58:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T16:59:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:00:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:01:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:02:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:03:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:04:51.462Z', + 'value': '1.2952504309564854' + }, + { + 'time': '2017-08-27T17:05:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:06:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:07:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:08:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:09:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:10:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:11:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:12:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:13:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:14:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:15:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:16:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:17:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:18:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:19:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:20:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:21:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:22:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:23:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:24:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:25:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:26:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:27:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:28:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:29:51.462Z', + 'value': '1.295225759754669' + }, + { + 'time': '2017-08-27T17:30:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:31:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:32:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:33:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:34:51.462Z', + 'value': '1.295225759754669' + }, + { + 'time': '2017-08-27T17:35:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:36:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:37:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:38:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:39:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:40:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:41:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:42:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:43:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:44:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:45:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:46:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:47:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:48:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:49:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:50:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:51:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:52:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:53:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:54:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:55:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T17:56:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:57:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T17:58:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T17:59:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T18:00:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:01:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:02:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:03:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:04:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:05:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:06:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:07:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:08:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:09:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:10:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:11:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:12:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T18:13:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:14:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:15:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:16:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:17:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:18:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:19:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:20:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:21:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:22:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:23:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:24:51.462Z', + 'value': '1.2571428571428571' + }, + { + 'time': '2017-08-27T18:25:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:26:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:27:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:28:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:29:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:30:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:31:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:32:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:33:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:34:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:35:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:36:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:37:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T18:38:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:39:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:40:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:41:51.462Z', + 'value': '1.580952380952381' + }, + { + 'time': '2017-08-27T18:42:51.462Z', + 'value': '1.7333333333333334' + }, + { + 'time': '2017-08-27T18:43:51.462Z', + 'value': '2.057142857142857' + }, + { + 'time': '2017-08-27T18:44:51.462Z', + 'value': '2.1904761904761902' + }, + { + 'time': '2017-08-27T18:45:51.462Z', + 'value': '1.8285714285714287' + }, + { + 'time': '2017-08-27T18:46:51.462Z', + 'value': '2.1142857142857143' + }, + { + 'time': '2017-08-27T18:47:51.462Z', + 'value': '1.619047619047619' + }, + { + 'time': '2017-08-27T18:48:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:49:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:50:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T18:51:51.462Z', + 'value': '1.2952504309564854' + }, + { + 'time': '2017-08-27T18:52:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:53:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:54:51.462Z', + 'value': '1.3333333333333333' + }, + { + 'time': '2017-08-27T18:55:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:56:51.462Z', + 'value': '1.314285714285714' + }, + { + 'time': '2017-08-27T18:57:51.462Z', + 'value': '1.295238095238095' + }, + { + 'time': '2017-08-27T18:58:51.462Z', + 'value': '1.7142857142857142' + }, + { + 'time': '2017-08-27T18:59:51.462Z', + 'value': '1.7333333333333334' + }, + { + 'time': '2017-08-27T19:00:51.462Z', + 'value': '1.3904761904761904' + }, + { + 'time': '2017-08-27T19:01:51.462Z', + 'value': '1.5047619047619047' + } + ] + }, + ] + } ] - } - ] - }, - { - 'title': 'Memory usage', - 'weight': 1, - 'y_label': 'Values', - 'queries': [ - { - 'query_range': 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', - 'label': 'Container memory', - 'unit': 'MiB', - 'result': [ - { - 'metric': { + }, + { + 'title': 'Throughput', + 'weight': 1, + 'y_label': 'Requests / Sec', + 'queries': [ + { + 'query_range': 'sum(rate(nginx_requests_total{server_zone!=\'*\', server_zone!=\'_\', container_name!=\'POD\',environment=\'production\'}[2m]))', + 'label': 'Total', + 'unit': 'req / sec', + 'result': [ + { + 'metric': { - }, - 'values': [ - { - 'time': '2017-06-04T21:22:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:23:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:24:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:25:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:26:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:27:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:28:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:29:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:30:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:31:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:32:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:33:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:34:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:35:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:36:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:37:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:38:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:39:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:40:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:41:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:42:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:43:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:44:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:45:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:46:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:47:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:48:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:49:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:50:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:51:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:52:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:53:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:54:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:55:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:56:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:57:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:58:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T21:59:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:00:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:01:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:02:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:03:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:04:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:05:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:06:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:07:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:08:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:09:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:10:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:11:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:12:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:13:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:14:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:15:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:16:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:17:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:18:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:19:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:20:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:21:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:22:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:23:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:24:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:25:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:26:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:27:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:28:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:29:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:30:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:31:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:32:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:33:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:34:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:35:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:36:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:37:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:38:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:39:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:40:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:41:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:42:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:43:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:44:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:45:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:46:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:47:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:48:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:49:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:50:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:51:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:52:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:53:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:54:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:55:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:56:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:57:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:58:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T22:59:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:00:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:01:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:02:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:03:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:04:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:05:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:06:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:07:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:08:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:09:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:10:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:11:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:12:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:13:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:14:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:15:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:16:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:17:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:18:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:19:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:20:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:21:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:22:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:23:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:24:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:25:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:26:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:27:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:28:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:29:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:30:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:31:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:32:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:33:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:34:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:35:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:36:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:37:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:38:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:39:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:40:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:41:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:42:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:43:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:44:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:45:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:46:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:47:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:48:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:49:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:50:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:51:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:52:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:53:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:54:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:55:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:56:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:57:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:58:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-04T23:59:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:00:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:01:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:02:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:03:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:04:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:05:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:06:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:07:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:08:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:09:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:10:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:11:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:12:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:13:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:14:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:15:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:16:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:17:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:18:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:19:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:20:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:21:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:22:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:23:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:24:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:25:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:26:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:27:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:28:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:29:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:30:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:31:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:32:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:33:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:34:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:35:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:36:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:37:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:38:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:39:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:40:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:41:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:42:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:43:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:44:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:45:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:46:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:47:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:48:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:49:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:50:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:51:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:52:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:53:59.508Z', - 'value': '15.0859375' - }, - { - 'time': '2017-06-05T00:54:59.508Z', - 'value': '15.0859375' - } - ] - } + }, + 'values': [ + { + 'time': '2017-08-27T11:01:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:02:51.462Z', + 'value': '0.45714285714285713' + }, + { + 'time': '2017-08-27T11:03:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:04:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:05:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:06:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:07:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:08:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:09:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:10:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:11:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:12:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:13:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:14:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:15:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:16:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:17:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:18:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:19:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:20:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:21:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:22:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:23:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:24:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:25:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:26:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:27:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:28:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:29:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:30:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:31:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:32:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:33:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:34:51.462Z', + 'value': '0.4952333787297264' + }, + { + 'time': '2017-08-27T11:35:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:36:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:37:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:38:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:39:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:40:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:41:51.462Z', + 'value': '0.49524752852435283' + }, + { + 'time': '2017-08-27T11:42:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:43:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:44:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:45:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:46:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:47:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:48:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:49:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T11:50:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:51:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:52:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:53:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:54:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:55:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:56:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:57:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T11:58:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T11:59:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:00:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:01:51.462Z', + 'value': '0.49524281183630325' + }, + { + 'time': '2017-08-27T12:02:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:03:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:04:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:05:51.462Z', + 'value': '0.4857096599080009' + }, + { + 'time': '2017-08-27T12:06:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:07:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:08:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:09:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:10:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:11:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:12:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:13:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:14:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:15:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:16:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:17:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:18:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:19:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:20:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:21:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:22:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:23:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:24:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:25:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:26:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:27:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:28:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:29:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:30:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:31:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:32:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:33:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:34:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:35:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:36:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:37:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:38:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:39:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:40:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:41:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:42:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:43:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:44:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:45:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:46:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:47:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:48:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:49:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:50:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:51:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:52:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:53:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:54:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T12:55:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:56:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:57:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T12:58:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T12:59:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:00:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:01:51.462Z', + 'value': '0.4761859410862754' + }, + { + 'time': '2017-08-27T13:02:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:03:51.462Z', + 'value': '0.4761995466580315' + }, + { + 'time': '2017-08-27T13:04:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:05:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:06:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:07:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:08:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:09:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:10:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:11:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:12:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:13:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:14:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:15:51.462Z', + 'value': '0.45714285714285713' + }, + { + 'time': '2017-08-27T13:16:51.462Z', + 'value': '0.49524752852435283' + }, + { + 'time': '2017-08-27T13:17:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:18:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:19:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:20:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:21:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:22:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:23:51.462Z', + 'value': '0.4666666666666667' + }, + { + 'time': '2017-08-27T13:24:51.462Z', + 'value': '0.45714285714285713' + }, + { + 'time': '2017-08-27T13:25:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:26:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:27:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:28:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:29:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:30:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:31:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:32:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:33:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:34:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:35:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:36:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:37:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:38:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:39:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:40:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:41:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:42:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:43:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:44:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:45:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:46:51.462Z', + 'value': '0.45714285714285713' + }, + { + 'time': '2017-08-27T13:47:51.462Z', + 'value': '0.4666666666666667' + }, + { + 'time': '2017-08-27T13:48:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:49:51.462Z', + 'value': '0.4761859410862754' + }, + { + 'time': '2017-08-27T13:50:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:51:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:52:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:53:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:54:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:55:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:56:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T13:57:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T13:58:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T13:59:51.462Z', + 'value': '0.4761859410862754' + }, + { + 'time': '2017-08-27T14:00:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:01:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:02:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:03:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:04:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:05:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:06:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:07:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:08:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:09:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:10:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:11:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:12:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:13:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:14:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:15:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:16:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:17:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:18:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:19:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:20:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:21:51.462Z', + 'value': '0.4952286623111941' + }, + { + 'time': '2017-08-27T14:22:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:23:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:24:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:25:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:26:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:27:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:28:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:29:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:30:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:31:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:32:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:33:51.462Z', + 'value': '0.45714285714285713' + }, + { + 'time': '2017-08-27T14:34:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:35:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:36:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:37:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:38:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:39:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:40:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:41:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:42:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:43:51.462Z', + 'value': '0.4666666666666667' + }, + { + 'time': '2017-08-27T14:44:51.462Z', + 'value': '0.45714285714285713' + }, + { + 'time': '2017-08-27T14:45:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:46:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:47:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:48:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:49:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:50:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:51:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:52:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:53:51.462Z', + 'value': '0.4952333787297264' + }, + { + 'time': '2017-08-27T14:54:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:55:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T14:56:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:57:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T14:58:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T14:59:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T15:00:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:01:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:02:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:03:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:04:51.462Z', + 'value': '0.45714285714285713' + }, + { + 'time': '2017-08-27T15:05:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:06:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:07:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:08:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:09:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:10:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:11:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:12:51.462Z', + 'value': '0.4857096599080009' + }, + { + 'time': '2017-08-27T15:13:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:14:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:15:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:16:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T15:17:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:18:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:19:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:20:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T15:21:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:22:51.462Z', + 'value': '0.49524281183630325' + }, + { + 'time': '2017-08-27T15:23:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:24:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:25:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:26:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:27:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:28:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:29:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:30:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:31:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T15:32:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:33:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T15:34:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:35:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T15:36:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:37:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:38:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T15:39:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:40:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:41:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:42:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:43:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:44:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:45:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:46:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:47:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:48:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:49:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T15:50:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:51:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:52:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:53:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:54:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:55:51.462Z', + 'value': '0.49524752852435283' + }, + { + 'time': '2017-08-27T15:56:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T15:57:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:58:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T15:59:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:00:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:01:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:02:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:03:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:04:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:05:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:06:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:07:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:08:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:09:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:10:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:11:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:12:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:13:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:14:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:15:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:16:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:17:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:18:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:19:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:20:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:21:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:22:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:23:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:24:51.462Z', + 'value': '0.4761859410862754' + }, + { + 'time': '2017-08-27T16:25:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:26:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:27:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:28:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:29:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:30:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:31:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:32:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:33:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:34:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:35:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:36:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:37:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:38:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:39:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:40:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:41:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:42:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:43:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:44:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:45:51.462Z', + 'value': '0.485718911608682' + }, + { + 'time': '2017-08-27T16:46:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:47:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:48:51.462Z', + 'value': '0.4952333787297264' + }, + { + 'time': '2017-08-27T16:49:51.462Z', + 'value': '0.4857096599080009' + }, + { + 'time': '2017-08-27T16:50:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:51:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:52:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:53:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:54:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:55:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T16:56:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:57:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T16:58:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T16:59:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:00:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:01:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:02:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:03:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:04:51.462Z', + 'value': '0.47619501138106085' + }, + { + 'time': '2017-08-27T17:05:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:06:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:07:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:08:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:09:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:10:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:11:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:12:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:13:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:14:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:15:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:16:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:17:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:18:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:19:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:20:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:21:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:22:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:23:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:24:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:25:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:26:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:27:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:28:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:29:51.462Z', + 'value': '0.4761859410862754' + }, + { + 'time': '2017-08-27T17:30:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:31:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:32:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:33:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:34:51.462Z', + 'value': '0.4761859410862754' + }, + { + 'time': '2017-08-27T17:35:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:36:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:37:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:38:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:39:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:40:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:41:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:42:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:43:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:44:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:45:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:46:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:47:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:48:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:49:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:50:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:51:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:52:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:53:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:54:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:55:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T17:56:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:57:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T17:58:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T17:59:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T18:00:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:01:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:02:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:03:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:04:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:05:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:06:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:07:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:08:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:09:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:10:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:11:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:12:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T18:13:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:14:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:15:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:16:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:17:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:18:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:19:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:20:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:21:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:22:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:23:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:24:51.462Z', + 'value': '0.45714285714285713' + }, + { + 'time': '2017-08-27T18:25:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:26:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:27:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:28:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:29:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:30:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:31:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:32:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:33:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:34:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:35:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:36:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:37:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T18:38:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:39:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:40:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:41:51.462Z', + 'value': '0.6190476190476191' + }, + { + 'time': '2017-08-27T18:42:51.462Z', + 'value': '0.6952380952380952' + }, + { + 'time': '2017-08-27T18:43:51.462Z', + 'value': '0.857142857142857' + }, + { + 'time': '2017-08-27T18:44:51.462Z', + 'value': '0.9238095238095239' + }, + { + 'time': '2017-08-27T18:45:51.462Z', + 'value': '0.7428571428571429' + }, + { + 'time': '2017-08-27T18:46:51.462Z', + 'value': '0.8857142857142857' + }, + { + 'time': '2017-08-27T18:47:51.462Z', + 'value': '0.638095238095238' + }, + { + 'time': '2017-08-27T18:48:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:49:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:50:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T18:51:51.462Z', + 'value': '0.47619501138106085' + }, + { + 'time': '2017-08-27T18:52:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:53:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:54:51.462Z', + 'value': '0.4952380952380952' + }, + { + 'time': '2017-08-27T18:55:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:56:51.462Z', + 'value': '0.4857142857142857' + }, + { + 'time': '2017-08-27T18:57:51.462Z', + 'value': '0.47619047619047616' + }, + { + 'time': '2017-08-27T18:58:51.462Z', + 'value': '0.6857142857142856' + }, + { + 'time': '2017-08-27T18:59:51.462Z', + 'value': '0.6952380952380952' + }, + { + 'time': '2017-08-27T19:00:51.462Z', + 'value': '0.5238095238095237' + }, + { + 'time': '2017-08-27T19:01:51.462Z', + 'value': '0.5904761904761905' + } + ] + } + ] + } ] - } - ] - } + } ]; +export function convertDatesMultipleSeries(multipleSeries) { + const convertedMultiple = multipleSeries; + multipleSeries.forEach((column, index) => { + let convertedResult = []; + convertedResult = column.queries[0].result.map((resultObj) => { + const convertedMetrics = {}; + convertedMetrics.values = resultObj.values.map(val => ({ + time: new Date(val.time), + value: val.value, + })); + convertedMetrics.metric = resultObj.metric; + return convertedMetrics; + }); + convertedMultiple[index].queries[0].result = convertedResult; + }); + return convertedMultiple; +} + export function MonitorMockInterceptor(request, next) { const body = responseMockData[request.method.toUpperCase()][request.url]; diff --git a/spec/javascripts/monitoring/monitoring_paths_spec.js b/spec/javascripts/monitoring/monitoring_paths_spec.js new file mode 100644 index 00000000000..d39db945e17 --- /dev/null +++ b/spec/javascripts/monitoring/monitoring_paths_spec.js @@ -0,0 +1,34 @@ +import Vue from 'vue'; +import MonitoringPaths from '~/monitoring/components/monitoring_paths.vue'; +import createTimeSeries from '~/monitoring/utils/multiple_time_series'; +import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data'; + +const createComponent = (propsData) => { + const Component = Vue.extend(MonitoringPaths); + + return new Component({ + propsData, + }).$mount(); +}; + +const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); + +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120); + +describe('Monitoring Paths', () => { + it('renders two paths to represent a line and the area underneath it', () => { + const component = createComponent({ + generatedLinePath: timeSeries[0].linePath, + generatedAreaPath: timeSeries[0].areaPath, + lineColor: '#ccc', + areaColor: '#fff', + }); + const metricArea = component.$el.querySelector('.metric-area'); + const metricLine = component.$el.querySelector('.metric-line'); + + expect(metricArea.getAttribute('fill')).toBe('#fff'); + expect(metricArea.getAttribute('d')).toBe(timeSeries[0].areaPath); + expect(metricLine.getAttribute('stroke')).toBe('#ccc'); + expect(metricLine.getAttribute('d')).toBe(timeSeries[0].linePath); + }); +}); diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js index 20c1e6a0005..88aa7659275 100644 --- a/spec/javascripts/monitoring/monitoring_store_spec.js +++ b/spec/javascripts/monitoring/monitoring_store_spec.js @@ -5,10 +5,10 @@ describe('MonitoringStore', () => { this.store = new MonitoringStore(); this.store.storeMetrics(MonitoringMock.data); - it('contains one group that contains two queries sorted by priority in one row', () => { + it('contains one group that contains two queries sorted by priority', () => { expect(this.store.groups).toBeDefined(); expect(this.store.groups.length).toEqual(1); - expect(this.store.groups[0].metrics.length).toEqual(1); + expect(this.store.groups[0].metrics.length).toEqual(2); }); it('gets the metrics count for every group', () => { diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js new file mode 100644 index 00000000000..3daf6bf82df --- /dev/null +++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js @@ -0,0 +1,21 @@ +import createTimeSeries from '~/monitoring/utils/multiple_time_series'; +import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data'; + +const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); +const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120); + +describe('Multiple time series', () => { + it('createTimeSeries returned array contains an object for each element', () => { + expect(typeof timeSeries[0].linePath).toEqual('string'); + expect(typeof timeSeries[0].areaPath).toEqual('string'); + expect(typeof timeSeries[0].timeSeriesScaleX).toEqual('function'); + expect(typeof timeSeries[0].areaColor).toEqual('string'); + expect(typeof timeSeries[0].lineColor).toEqual('string'); + expect(timeSeries[0].values instanceof Array).toEqual(true); + }); + + it('createTimeSeries returns an array', () => { + expect(timeSeries instanceof Array).toEqual(true); + expect(timeSeries.length).toEqual(2); + }); +}); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js deleted file mode 100644 index 3d36bb3e4d4..00000000000 --- a/spec/javascripts/project_title_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -/* global Project */ - -import 'select2/select2'; -import '~/gl_dropdown'; -import '~/api'; -import '~/project_select'; -import '~/project'; - -describe('Project Title', () => { - const dummyApiVersion = 'v3000'; - preloadFixtures('issues/open-issue.html.raw'); - loadJSONFixtures('projects.json'); - - beforeEach(() => { - loadFixtures('issues/open-issue.html.raw'); - - window.gon = {}; - window.gon.api_version = dummyApiVersion; - - // eslint-disable-next-line no-new - new Project(); - }); - - describe('project list', () => { - let reqUrl; - let reqData; - - beforeEach(() => { - const fakeResponseData = getJSONFixture('projects.json'); - spyOn(jQuery, 'ajax').and.callFake((req) => { - const def = $.Deferred(); - reqUrl = req.url; - reqData = req.data; - def.resolve(fakeResponseData); - return def.promise(); - }); - }); - - it('toggles dropdown', () => { - const $menu = $('.js-dropdown-menu-projects'); - window.gon.current_user_id = 1; - $('.js-projects-dropdown-toggle').click(); - expect($menu).toHaveClass('open'); - expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json?simple=true`); - expect(reqData).toEqual({ - search: '', - order_by: 'last_activity_at', - per_page: 20, - membership: true, - }); - $menu.find('.dropdown-menu-close-icon').click(); - expect($menu).not.toHaveClass('open'); - }); - }); - - afterEach(() => { - window.gon = {}; - }); -}); diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js new file mode 100644 index 00000000000..42f0f6fc1af --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/app_spec.js @@ -0,0 +1,348 @@ +import Vue from 'vue'; + +import bp from '~/breakpoints'; +import appComponent from '~/projects_dropdown/components/app.vue'; +import eventHub from '~/projects_dropdown/event_hub'; +import ProjectsStore from '~/projects_dropdown/store/projects_store'; +import ProjectsService from '~/projects_dropdown/service/projects_service'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { currentSession, mockProject, mockRawProject } from '../mock_data'; + +const createComponent = () => { + gon.api_version = currentSession.apiVersion; + const Component = Vue.extend(appComponent); + const store = new ProjectsStore(); + const service = new ProjectsService(currentSession.username); + + return mountComponent(Component, { + store, + service, + currentUserName: currentSession.username, + currentProject: currentSession.project, + }); +}; + +const returnServicePromise = (data, failed) => new Promise((resolve, reject) => { + if (failed) { + reject(data); + } else { + resolve({ + json() { + return data; + }, + }); + } +}); + +describe('AppComponent', () => { + describe('computed', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('frequentProjects', () => { + it('should return list of frequently accessed projects from store', () => { + expect(vm.frequentProjects).toBeDefined(); + expect(vm.frequentProjects.length).toBe(0); + + vm.store.setFrequentProjects([mockProject]); + expect(vm.frequentProjects).toBeDefined(); + expect(vm.frequentProjects.length).toBe(1); + }); + }); + + describe('searchProjects', () => { + it('should return list of frequently accessed projects from store', () => { + expect(vm.searchProjects).toBeDefined(); + expect(vm.searchProjects.length).toBe(0); + + vm.store.setSearchedProjects([mockRawProject]); + expect(vm.searchProjects).toBeDefined(); + expect(vm.searchProjects.length).toBe(1); + }); + }); + }); + + describe('methods', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('toggleFrequentProjectsList', () => { + it('should toggle props which control visibility of Frequent Projects list from state passed', () => { + vm.toggleFrequentProjectsList(true); + expect(vm.isLoadingProjects).toBeFalsy(); + expect(vm.isSearchListVisible).toBeFalsy(); + expect(vm.isFrequentsListVisible).toBeTruthy(); + + vm.toggleFrequentProjectsList(false); + expect(vm.isLoadingProjects).toBeTruthy(); + expect(vm.isSearchListVisible).toBeTruthy(); + expect(vm.isFrequentsListVisible).toBeFalsy(); + }); + }); + + describe('toggleSearchProjectsList', () => { + it('should toggle props which control visibility of Searched Projects list from state passed', () => { + vm.toggleSearchProjectsList(true); + expect(vm.isLoadingProjects).toBeFalsy(); + expect(vm.isFrequentsListVisible).toBeFalsy(); + expect(vm.isSearchListVisible).toBeTruthy(); + + vm.toggleSearchProjectsList(false); + expect(vm.isLoadingProjects).toBeTruthy(); + expect(vm.isFrequentsListVisible).toBeTruthy(); + expect(vm.isSearchListVisible).toBeFalsy(); + }); + }); + + describe('toggleLoader', () => { + it('should toggle props which control visibility of list loading animation from state passed', () => { + vm.toggleLoader(true); + expect(vm.isFrequentsListVisible).toBeFalsy(); + expect(vm.isSearchListVisible).toBeFalsy(); + expect(vm.isLoadingProjects).toBeTruthy(); + + vm.toggleLoader(false); + expect(vm.isFrequentsListVisible).toBeTruthy(); + expect(vm.isSearchListVisible).toBeTruthy(); + expect(vm.isLoadingProjects).toBeFalsy(); + }); + }); + + describe('fetchFrequentProjects', () => { + it('should set props for loading animation to `true` while frequent projects list is being loaded', () => { + spyOn(vm, 'toggleLoader'); + + vm.fetchFrequentProjects(); + expect(vm.isLocalStorageFailed).toBeFalsy(); + expect(vm.toggleLoader).toHaveBeenCalledWith(true); + }); + + it('should set props for loading animation to `false` and props for frequent projects list to `true` once data is loaded', () => { + const mockData = [mockProject]; + + spyOn(vm.service, 'getFrequentProjects').and.returnValue(mockData); + spyOn(vm.store, 'setFrequentProjects'); + spyOn(vm, 'toggleFrequentProjectsList'); + + vm.fetchFrequentProjects(); + expect(vm.service.getFrequentProjects).toHaveBeenCalled(); + expect(vm.store.setFrequentProjects).toHaveBeenCalledWith(mockData); + expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true); + }); + + it('should set props for failure message to `true` when method fails to fetch frequent projects list', () => { + spyOn(vm.service, 'getFrequentProjects').and.returnValue(null); + spyOn(vm.store, 'setFrequentProjects'); + spyOn(vm, 'toggleFrequentProjectsList'); + + expect(vm.isLocalStorageFailed).toBeFalsy(); + + vm.fetchFrequentProjects(); + expect(vm.service.getFrequentProjects).toHaveBeenCalled(); + expect(vm.store.setFrequentProjects).toHaveBeenCalledWith([]); + expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true); + expect(vm.isLocalStorageFailed).toBeTruthy(); + }); + + it('should set props for search results list to `true` if search query was already made previously', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('md'); + spyOn(vm.service, 'getFrequentProjects'); + spyOn(vm, 'toggleSearchProjectsList'); + + vm.searchQuery = 'test'; + vm.fetchFrequentProjects(); + expect(vm.service.getFrequentProjects).not.toHaveBeenCalled(); + expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true); + }); + + it('should set props for frequent projects list to `true` if search query was already made but screen size is less than 768px', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); + spyOn(vm, 'toggleSearchProjectsList'); + spyOn(vm.service, 'getFrequentProjects'); + + vm.searchQuery = 'test'; + vm.fetchFrequentProjects(); + expect(vm.service.getFrequentProjects).toHaveBeenCalled(); + expect(vm.toggleSearchProjectsList).not.toHaveBeenCalled(); + }); + }); + + describe('fetchSearchedProjects', () => { + const searchQuery = 'test'; + + it('should perform search with provided search query', (done) => { + const mockData = [mockRawProject]; + spyOn(vm, 'toggleLoader'); + spyOn(vm, 'toggleSearchProjectsList'); + spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise(mockData)); + spyOn(vm.store, 'setSearchedProjects'); + + vm.fetchSearchedProjects(searchQuery); + setTimeout(() => { + expect(vm.searchQuery).toBe(searchQuery); + expect(vm.toggleLoader).toHaveBeenCalledWith(true); + expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery); + expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true); + expect(vm.store.setSearchedProjects).toHaveBeenCalledWith(mockData); + done(); + }, 0); + }); + + it('should update props for showing search failure', (done) => { + spyOn(vm, 'toggleSearchProjectsList'); + spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise({}, true)); + + vm.fetchSearchedProjects(searchQuery); + setTimeout(() => { + expect(vm.searchQuery).toBe(searchQuery); + expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery); + expect(vm.isSearchFailed).toBeTruthy(); + expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true); + done(); + }, 0); + }); + }); + + describe('logCurrentProjectAccess', () => { + it('should log current project access via service', (done) => { + spyOn(vm.service, 'logProjectAccess'); + + vm.currentProject = mockProject; + vm.logCurrentProjectAccess(); + + setTimeout(() => { + expect(vm.service.logProjectAccess).toHaveBeenCalledWith(mockProject); + done(); + }, 1); + }); + }); + + describe('handleSearchClear', () => { + it('should show frequent projects list when search input is cleared', () => { + spyOn(vm.store, 'clearSearchedProjects'); + spyOn(vm, 'toggleFrequentProjectsList'); + + vm.handleSearchClear(); + + expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true); + expect(vm.store.clearSearchedProjects).toHaveBeenCalled(); + expect(vm.searchQuery).toBe(''); + }); + }); + + describe('handleSearchFailure', () => { + it('should show failure message within dropdown', () => { + spyOn(vm, 'toggleSearchProjectsList'); + + vm.handleSearchFailure(); + expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true); + expect(vm.isSearchFailed).toBeTruthy(); + }); + }); + }); + + describe('created', () => { + it('should bind event listeners on eventHub', (done) => { + spyOn(eventHub, '$on'); + + createComponent().$mount(); + + Vue.nextTick(() => { + expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('searchProjects', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('searchCleared', jasmine.any(Function)); + expect(eventHub.$on).toHaveBeenCalledWith('searchFailed', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('beforeDestroy', () => { + it('should unbind event listeners on eventHub', (done) => { + const vm = createComponent(); + spyOn(eventHub, '$off'); + + vm.$mount(); + vm.$destroy(); + + Vue.nextTick(() => { + expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('searchProjects', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('searchCleared', jasmine.any(Function)); + expect(eventHub.$off).toHaveBeenCalledWith('searchFailed', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('template', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render search input', () => { + expect(vm.$el.querySelector('.search-input-container')).toBeDefined(); + }); + + it('should render loading animation', (done) => { + vm.toggleLoader(true); + Vue.nextTick(() => { + const loadingEl = vm.$el.querySelector('.loading-animation'); + + expect(loadingEl).toBeDefined(); + expect(loadingEl.classList.contains('prepend-top-20')).toBeTruthy(); + expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects'); + done(); + }); + }); + + it('should render frequent projects list header', (done) => { + vm.toggleFrequentProjectsList(true); + Vue.nextTick(() => { + const sectionHeaderEl = vm.$el.querySelector('.section-header'); + + expect(sectionHeaderEl).toBeDefined(); + expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited'); + done(); + }); + }); + + it('should render frequent projects list', (done) => { + vm.toggleFrequentProjectsList(true); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.projects-list-frequent-container')).toBeDefined(); + done(); + }); + }); + + it('should render searched projects list', (done) => { + vm.toggleSearchProjectsList(true); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.section-header')).toBe(null); + expect(vm.$el.querySelector('.projects-list-search-container')).toBeDefined(); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js new file mode 100644 index 00000000000..fcd0f6a3630 --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js @@ -0,0 +1,72 @@ +import Vue from 'vue'; + +import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { mockFrequents } from '../mock_data'; + +const createComponent = () => { + const Component = Vue.extend(projectsListFrequentComponent); + + return mountComponent(Component, { + projects: mockFrequents, + localStorageFailed: false, + }); +}; + +describe('ProjectsListFrequentComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('isListEmpty', () => { + it('should return `true` or `false` representing whether if `projects` is empty of not', () => { + vm.projects = []; + expect(vm.isListEmpty).toBeTruthy(); + + vm.projects = mockFrequents; + expect(vm.isListEmpty).toBeFalsy(); + }); + }); + + describe('listEmptyMessage', () => { + it('should return appropriate empty list message based on value of `localStorageFailed` prop', () => { + vm.localStorageFailed = true; + expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support'); + + vm.localStorageFailed = false; + expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here'); + }); + }); + }); + + describe('template', () => { + it('should render component element with list of projects', (done) => { + vm.projects = mockFrequents; + + Vue.nextTick(() => { + expect(vm.$el.classList.contains('projects-list-frequent-container')).toBeTruthy(); + expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(5); + done(); + }); + }); + + it('should render component element with empty message', (done) => { + vm.projects = []; + + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js new file mode 100644 index 00000000000..171629fcd6b --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js @@ -0,0 +1,65 @@ +import Vue from 'vue'; + +import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { mockProject } from '../mock_data'; + +const createComponent = () => { + const Component = Vue.extend(projectsListItemComponent); + + return mountComponent(Component, { + projectId: mockProject.id, + projectName: mockProject.name, + namespace: mockProject.namespace, + webUrl: mockProject.webUrl, + avatarUrl: mockProject.avatarUrl, + }); +}; + +describe('ProjectsListItemComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('hasAvatar', () => { + it('should return `true` or `false` if whether avatar is present or not', () => { + vm.avatarUrl = 'path/to/avatar.png'; + expect(vm.hasAvatar).toBeTruthy(); + + vm.avatarUrl = null; + expect(vm.hasAvatar).toBeFalsy(); + }); + }); + + describe('highlightedProjectName', () => { + it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => { + vm.matcher = 'lab'; + expect(vm.highlightedProjectName).toContain('<b>Lab</b>'); + }); + + it('should return project name as it is if `matcher` is not available', () => { + vm.matcher = null; + expect(vm.highlightedProjectName).toBe(mockProject.name); + }); + }); + }); + + describe('template', () => { + it('should render component element', () => { + expect(vm.$el.classList.contains('projects-list-item-container')).toBeTruthy(); + expect(vm.$el.querySelectorAll('a').length).toBe(1); + expect(vm.$el.querySelectorAll('.project-item-avatar-container').length).toBe(1); + expect(vm.$el.querySelectorAll('.project-item-metadata-container').length).toBe(1); + expect(vm.$el.querySelectorAll('.project-title').length).toBe(1); + expect(vm.$el.querySelectorAll('.project-namespace').length).toBe(1); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js new file mode 100644 index 00000000000..59fc2dedba5 --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js @@ -0,0 +1,84 @@ +import Vue from 'vue'; + +import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; +import { mockProject } from '../mock_data'; + +const createComponent = () => { + const Component = Vue.extend(projectsListSearchComponent); + + return mountComponent(Component, { + projects: [mockProject], + matcher: 'lab', + searchFailed: false, + }); +}; + +describe('ProjectsListSearchComponent', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('isListEmpty', () => { + it('should return `true` or `false` representing whether if `projects` is empty of not', () => { + vm.projects = []; + expect(vm.isListEmpty).toBeTruthy(); + + vm.projects = [mockProject]; + expect(vm.isListEmpty).toBeFalsy(); + }); + }); + + describe('listEmptyMessage', () => { + it('should return appropriate empty list message based on value of `searchFailed` prop', () => { + vm.searchFailed = true; + expect(vm.listEmptyMessage).toBe('Something went wrong on our end.'); + + vm.searchFailed = false; + expect(vm.listEmptyMessage).toBe('No projects matched your query'); + }); + }); + }); + + describe('template', () => { + it('should render component element with list of projects', (done) => { + vm.projects = [mockProject]; + + Vue.nextTick(() => { + expect(vm.$el.classList.contains('projects-list-search-container')).toBeTruthy(); + expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(1); + done(); + }); + }); + + it('should render component element with empty message', (done) => { + vm.projects = []; + + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0); + done(); + }); + }); + + it('should render component element with failure message', (done) => { + vm.searchFailed = true; + vm.projects = []; + + Vue.nextTick(() => { + expect(vm.$el.querySelectorAll('li.section-empty.section-failure').length).toBe(1); + expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0); + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js new file mode 100644 index 00000000000..f2a23e33325 --- /dev/null +++ b/spec/javascripts/projects_dropdown/components/search_spec.js @@ -0,0 +1,101 @@ +import Vue from 'vue'; + +import searchComponent from '~/projects_dropdown/components/search.vue'; +import eventHub from '~/projects_dropdown/event_hub'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = () => { + const Component = Vue.extend(searchComponent); + + return mountComponent(Component); +}; + +describe('SearchComponent', () => { + describe('methods', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('setFocus', () => { + it('should set focus to search input', () => { + spyOn(vm.$refs.search, 'focus'); + + vm.setFocus(); + expect(vm.$refs.search.focus).toHaveBeenCalled(); + }); + }); + + describe('emitSearchEvents', () => { + it('should emit `searchProjects` event via eventHub when `searchQuery` present', () => { + const searchQuery = 'test'; + spyOn(eventHub, '$emit'); + vm.searchQuery = searchQuery; + vm.emitSearchEvents(); + expect(eventHub.$emit).toHaveBeenCalledWith('searchProjects', searchQuery); + }); + + it('should emit `searchCleared` event via eventHub when `searchQuery` is cleared', () => { + spyOn(eventHub, '$emit'); + vm.searchQuery = ''; + vm.emitSearchEvents(); + expect(eventHub.$emit).toHaveBeenCalledWith('searchCleared'); + }); + }); + }); + + describe('mounted', () => { + it('should listen `dropdownOpen` event', (done) => { + spyOn(eventHub, '$on'); + createComponent(); + + Vue.nextTick(() => { + expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('beforeDestroy', () => { + it('should unbind event listeners on eventHub', (done) => { + const vm = createComponent(); + spyOn(eventHub, '$off'); + + vm.$mount(); + vm.$destroy(); + + Vue.nextTick(() => { + expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function)); + done(); + }); + }); + }); + + describe('template', () => { + let vm; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render component element', () => { + const inputEl = vm.$el.querySelector('input.form-control'); + + expect(vm.$el.classList.contains('search-input-container')).toBeTruthy(); + expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy(); + expect(inputEl).not.toBe(null); + expect(inputEl.getAttribute('placeholder')).toBe('Search projects'); + expect(vm.$el.querySelector('.search-icon')).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/mock_data.js b/spec/javascripts/projects_dropdown/mock_data.js new file mode 100644 index 00000000000..d6a79fb8ac1 --- /dev/null +++ b/spec/javascripts/projects_dropdown/mock_data.js @@ -0,0 +1,96 @@ +export const currentSession = { + username: 'root', + storageKey: 'root/frequent-projects', + apiVersion: 'v4', + project: { + id: 1, + name: 'dummy-project', + namespace: 'SamepleGroup / Dummy-Project', + webUrl: 'http://127.0.0.1/samplegroup/dummy-project', + avatarUrl: null, + lastAccessedOn: Date.now(), + }, +}; + +export const mockProject = { + id: 1, + name: 'GitLab Community Edition', + namespace: 'gitlab-org / gitlab-ce', + webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce', + avatarUrl: null, +}; + +export const mockRawProject = { + id: 1, + name: 'GitLab Community Edition', + name_with_namespace: 'gitlab-org / gitlab-ce', + web_url: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce', + avatar_url: null, +}; + +export const mockFrequents = [ + { + id: 1, + name: 'GitLab Community Edition', + namespace: 'gitlab-org / gitlab-ce', + webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce', + avatarUrl: null, + }, + { + id: 2, + name: 'GitLab CI', + namespace: 'gitlab-org / gitlab-ci', + webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ci', + avatarUrl: null, + }, + { + id: 3, + name: 'Typeahead.Js', + namespace: 'twitter / typeahead-js', + webUrl: 'http://127.0.0.1:3000/twitter/typeahead-js', + avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png', + }, + { + id: 4, + name: 'Intel', + namespace: 'platform / hardware / bsp / intel', + webUrl: 'http://127.0.0.1:3000/platform/hardware/bsp/intel', + avatarUrl: null, + }, + { + id: 5, + name: 'v4.4', + namespace: 'platform / hardware / bsp / kernel / common / v4.4', + webUrl: 'http://localhost:3000/platform/hardware/bsp/kernel/common/v4.4', + avatarUrl: null, + }, +]; + +export const unsortedFrequents = [ + { id: 1, frequency: 12, lastAccessedOn: 1491400843391 }, + { id: 2, frequency: 14, lastAccessedOn: 1488240890738 }, + { id: 3, frequency: 44, lastAccessedOn: 1497675908472 }, + { id: 4, frequency: 8, lastAccessedOn: 1497979281815 }, + { id: 5, frequency: 34, lastAccessedOn: 1488089211943 }, + { id: 6, frequency: 14, lastAccessedOn: 1493517292488 }, + { id: 7, frequency: 42, lastAccessedOn: 1486815299875 }, + { id: 8, frequency: 33, lastAccessedOn: 1500762279114 }, + { id: 10, frequency: 46, lastAccessedOn: 1483251641543 }, +]; + +/** + * This const has a specific order which tests authenticity + * of `ProjectsService.getTopFrequentProjects` method so + * DO NOT change order of items in this const. + */ +export const sortedFrequents = [ + { id: 10, frequency: 46, lastAccessedOn: 1483251641543 }, + { id: 3, frequency: 44, lastAccessedOn: 1497675908472 }, + { id: 7, frequency: 42, lastAccessedOn: 1486815299875 }, + { id: 5, frequency: 34, lastAccessedOn: 1488089211943 }, + { id: 8, frequency: 33, lastAccessedOn: 1500762279114 }, + { id: 6, frequency: 14, lastAccessedOn: 1493517292488 }, + { id: 2, frequency: 14, lastAccessedOn: 1488240890738 }, + { id: 1, frequency: 12, lastAccessedOn: 1491400843391 }, + { id: 4, frequency: 8, lastAccessedOn: 1497979281815 }, +]; diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js new file mode 100644 index 00000000000..d5dd8b3449a --- /dev/null +++ b/spec/javascripts/projects_dropdown/service/projects_service_spec.js @@ -0,0 +1,179 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +import bp from '~/breakpoints'; +import ProjectsService from '~/projects_dropdown/service/projects_service'; +import { FREQUENT_PROJECTS } from '~/projects_dropdown/constants'; +import { currentSession, unsortedFrequents, sortedFrequents } from '../mock_data'; + +Vue.use(VueResource); + +FREQUENT_PROJECTS.MAX_COUNT = 3; + +describe('ProjectsService', () => { + let service; + + beforeEach(() => { + gon.api_version = currentSession.apiVersion; + gon.current_user_id = 1; + service = new ProjectsService(currentSession.username); + }); + + describe('contructor', () => { + it('should initialize default properties of class', () => { + expect(service.isLocalStorageAvailable).toBeTruthy(); + expect(service.currentUserName).toBe(currentSession.username); + expect(service.storageKey).toBe(currentSession.storageKey); + expect(service.projectsPath).toBeDefined(); + }); + }); + + describe('getSearchedProjects', () => { + it('should return promise from VueResource HTTP GET', () => { + spyOn(service.projectsPath, 'get').and.stub(); + + const searchQuery = 'lab'; + const queryParams = { + simple: false, + per_page: 20, + membership: true, + order_by: 'last_activity_at', + search: searchQuery, + }; + + service.getSearchedProjects(searchQuery); + expect(service.projectsPath.get).toHaveBeenCalledWith(queryParams); + }); + }); + + describe('logProjectAccess', () => { + let storage; + + beforeEach(() => { + storage = {}; + + spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => { + storage[storageKey] = value; + }); + + spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => { + if (storage[storageKey]) { + return storage[storageKey]; + } + + return null; + }); + }); + + it('should create a project store if it does not exist and adds a project', () => { + service.logProjectAccess(currentSession.project); + + const projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects.length).toBe(1); + expect(projects[0].frequency).toBe(1); + expect(projects[0].lastAccessedOn).toBeDefined(); + }); + + it('should prevent inserting same report multiple times into store', () => { + service.logProjectAccess(currentSession.project); + service.logProjectAccess(currentSession.project); + + const projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects.length).toBe(1); + }); + + it('should increase frequency of report if it was logged multiple times over the course of an hour', () => { + let projects; + spyOn(Math, 'abs').and.returnValue(3600001); // this will lead to `diff` > 1; + service.logProjectAccess(currentSession.project); + + projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects[0].frequency).toBe(1); + + service.logProjectAccess(currentSession.project); + projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects[0].frequency).toBe(2); + expect(projects[0].lastAccessedOn).not.toBe(currentSession.project.lastAccessedOn); + }); + + it('should always update project metadata', () => { + let projects; + const oldProject = { + ...currentSession.project, + }; + + const newProject = { + ...currentSession.project, + name: 'New Name', + avatarUrl: 'new/avatar.png', + namespace: 'New / Namespace', + webUrl: 'http://localhost/new/web/url', + }; + + service.logProjectAccess(oldProject); + projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects[0].name).toBe(oldProject.name); + expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl); + expect(projects[0].namespace).toBe(oldProject.namespace); + expect(projects[0].webUrl).toBe(oldProject.webUrl); + + service.logProjectAccess(newProject); + projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects[0].name).toBe(newProject.name); + expect(projects[0].avatarUrl).toBe(newProject.avatarUrl); + expect(projects[0].namespace).toBe(newProject.namespace); + expect(projects[0].webUrl).toBe(newProject.webUrl); + }); + + it('should not add more than 20 projects in store', () => { + for (let i = 1; i <= 5; i += 1) { + const project = Object.assign(currentSession.project, { id: i }); + service.logProjectAccess(project); + } + + const projects = JSON.parse(storage[currentSession.storageKey]); + expect(projects.length).toBe(3); + }); + }); + + describe('getTopFrequentProjects', () => { + let storage = {}; + + beforeEach(() => { + storage[currentSession.storageKey] = JSON.stringify(unsortedFrequents); + + spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => { + if (storage[storageKey]) { + return storage[storageKey]; + } + + return null; + }); + }); + + it('should return top 5 frequently accessed projects for desktop screens', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('md'); + const frequentProjects = service.getTopFrequentProjects(); + + expect(frequentProjects.length).toBe(5); + frequentProjects.forEach((project, index) => { + expect(project.id).toBe(sortedFrequents[index].id); + }); + }); + + it('should return top 3 frequently accessed projects for mobile screens', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); + const frequentProjects = service.getTopFrequentProjects(); + + expect(frequentProjects.length).toBe(3); + frequentProjects.forEach((project, index) => { + expect(project.id).toBe(sortedFrequents[index].id); + }); + }); + + it('should return empty array if there are no projects available in store', () => { + storage = {}; + expect(service.getTopFrequentProjects().length).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/projects_dropdown/store/projects_store_spec.js b/spec/javascripts/projects_dropdown/store/projects_store_spec.js new file mode 100644 index 00000000000..e57399d37cd --- /dev/null +++ b/spec/javascripts/projects_dropdown/store/projects_store_spec.js @@ -0,0 +1,41 @@ +import ProjectsStore from '~/projects_dropdown/store/projects_store'; +import { mockProject, mockRawProject } from '../mock_data'; + +describe('ProjectsStore', () => { + let store; + + beforeEach(() => { + store = new ProjectsStore(); + }); + + describe('setFrequentProjects', () => { + it('should set frequent projects list to state', () => { + store.setFrequentProjects([mockProject]); + + expect(store.getFrequentProjects().length).toBe(1); + expect(store.getFrequentProjects()[0].id).toBe(mockProject.id); + }); + }); + + describe('setSearchedProjects', () => { + it('should set searched projects list to state', () => { + store.setSearchedProjects([mockRawProject]); + + const processedProjects = store.getSearchedProjects(); + expect(processedProjects.length).toBe(1); + expect(processedProjects[0].id).toBe(mockRawProject.id); + expect(processedProjects[0].namespace).toBe(mockRawProject.name_with_namespace); + expect(processedProjects[0].webUrl).toBe(mockRawProject.web_url); + expect(processedProjects[0].avatarUrl).toBe(mockRawProject.avatar_url); + }); + }); + + describe('clearSearchedProjects', () => { + it('should clear searched projects list from state', () => { + store.setSearchedProjects([mockRawProject]); + expect(store.getSearchedProjects().length).toBe(1); + store.clearSearchedProjects(); + expect(store.getSearchedProjects().length).toBe(0); + }); + }); +}); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index 9fc8667ecc9..e2b6bcabc98 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -66,17 +66,57 @@ const sidebarMockData = { }, labels: [], }, + '/autocomplete/projects?project_id=15': [ + { + 'id': 0, + 'name_with_namespace': 'No project', + }, { + 'id': 20, + 'name_with_namespace': 'foo / bar', + }, + ], }, 'PUT': { '/gitlab-org/gitlab-shell/issues/5.json': { data: {}, }, }, + 'POST': { + '/gitlab-org/gitlab-shell/issues/5/move': { + id: 123, + iid: 5, + author_id: 1, + description: 'some description', + lock_version: 5, + milestone_id: null, + state: 'opened', + title: 'some title', + updated_by_id: 1, + created_at: '2017-06-27T19:54:42.437Z', + updated_at: '2017-08-18T03:39:49.222Z', + deleted_at: null, + time_estimate: 0, + total_time_spent: 0, + human_time_estimate: null, + human_total_time_spent: null, + branch_name: null, + confidential: false, + assignees: [], + due_date: null, + moved_to_id: null, + project_id: 7, + milestone: null, + labels: [], + web_url: '/root/some-project/issues/5', + }, + }, }; export default { mediator: { endpoint: '/gitlab-org/gitlab-shell/issues/5.json', + moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', + projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', editable: true, currentUser: { id: 1, @@ -85,6 +125,7 @@ export default { avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }, rootPath: '/', + fullPath: '/gitlab-org/gitlab-shell', }, time: { time_estimate: 3600, diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index e246f41ee82..3aa8ca5db0d 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -30,7 +30,7 @@ describe('Sidebar mediator', () => { expect(resp.status).toEqual(200); done(); }) - .catch(() => {}); + .catch(done.fail); }); it('fetches the data', () => { @@ -38,4 +38,42 @@ describe('Sidebar mediator', () => { this.mediator.fetch(); expect(this.mediator.service.get).toHaveBeenCalled(); }); + + it('sets moveToProjectId', () => { + const projectId = 7; + spyOn(this.mediator.store, 'setMoveToProjectId').and.callThrough(); + + this.mediator.setMoveToProjectId(projectId); + + expect(this.mediator.store.setMoveToProjectId).toHaveBeenCalledWith(projectId); + }); + + it('fetches autocomplete projects', (done) => { + const searchTerm = 'foo'; + spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough(); + spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough(); + + this.mediator.fetchAutocompleteProjects(searchTerm) + .then(() => { + expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm); + expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled(); + done(); + }) + .catch(done.fail); + }); + + it('moves issue', (done) => { + const moveToProjectId = 7; + this.mediator.store.setMoveToProjectId(moveToProjectId); + spyOn(this.mediator.service, 'moveIssue').and.callThrough(); + spyOn(gl.utils, 'visitUrl'); + + this.mediator.moveIssue() + .then(() => { + expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId); + expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); + done(); + }) + .catch(done.fail); + }); }); diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js new file mode 100644 index 00000000000..8b0d51bbcc8 --- /dev/null +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -0,0 +1,142 @@ +import Vue from 'vue'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; +import SidebarStore from '~/sidebar/stores/sidebar_store'; +import SidebarService from '~/sidebar/services/sidebar_service'; +import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue'; +import Mock from './mock_data'; + +describe('SidebarMoveIssue', () => { + beforeEach(() => { + Vue.http.interceptors.push(Mock.sidebarMockInterceptor); + this.mediator = new SidebarMediator(Mock.mediator); + this.$content = $(` + <div class="dropdown"> + <div class="js-toggle"></div> + <div class="dropdown-content"></div> + <div class="js-confirm-button"></div> + </div> + `); + this.$toggleButton = this.$content.find('.js-toggle'); + this.$confirmButton = this.$content.find('.js-confirm-button'); + + this.sidebarMoveIssue = new SidebarMoveIssue( + this.mediator, + this.$toggleButton, + this.$confirmButton, + ); + this.sidebarMoveIssue.init(); + }); + + afterEach(() => { + SidebarService.singleton = null; + SidebarStore.singleton = null; + SidebarMediator.singleton = null; + + this.sidebarMoveIssue.destroy(); + + Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor); + }); + + describe('init', () => { + it('should initialize the dropdown and listeners', () => { + spyOn(this.sidebarMoveIssue, 'initDropdown'); + spyOn(this.sidebarMoveIssue, 'addEventListeners'); + + this.sidebarMoveIssue.init(); + + expect(this.sidebarMoveIssue.initDropdown).toHaveBeenCalled(); + expect(this.sidebarMoveIssue.addEventListeners).toHaveBeenCalled(); + }); + }); + + describe('destroy', () => { + it('should remove the listeners', () => { + spyOn(this.sidebarMoveIssue, 'removeEventListeners'); + + this.sidebarMoveIssue.destroy(); + + expect(this.sidebarMoveIssue.removeEventListeners).toHaveBeenCalled(); + }); + }); + + describe('initDropdown', () => { + it('should initialize the gl_dropdown', () => { + spyOn($.fn, 'glDropdown'); + + this.sidebarMoveIssue.initDropdown(); + + expect($.fn.glDropdown).toHaveBeenCalled(); + }); + }); + + describe('onConfirmClicked', () => { + it('should move the issue with valid project ID', () => { + spyOn(this.mediator, 'moveIssue').and.returnValue(Promise.resolve()); + this.mediator.setMoveToProjectId(7); + + this.sidebarMoveIssue.onConfirmClicked(); + + expect(this.mediator.moveIssue).toHaveBeenCalled(); + expect(this.$confirmButton.attr('disabled')).toBe('disabled'); + expect(this.$confirmButton.hasClass('is-loading')).toBe(true); + }); + + it('should remove loading state from confirm button on failure', (done) => { + spyOn(window, 'Flash'); + spyOn(this.mediator, 'moveIssue').and.returnValue(Promise.reject()); + this.mediator.setMoveToProjectId(7); + + this.sidebarMoveIssue.onConfirmClicked(); + + expect(this.mediator.moveIssue).toHaveBeenCalled(); + // Wait for the move issue request to fail + setTimeout(() => { + expect(window.Flash).toHaveBeenCalled(); + expect(this.$confirmButton.attr('disabled')).toBe(undefined); + expect(this.$confirmButton.hasClass('is-loading')).toBe(false); + done(); + }); + }); + + it('should not move the issue with id=0', () => { + spyOn(this.mediator, 'moveIssue'); + this.mediator.setMoveToProjectId(0); + + this.sidebarMoveIssue.onConfirmClicked(); + + expect(this.mediator.moveIssue).not.toHaveBeenCalled(); + }); + }); + + it('should set moveToProjectId on dropdown item "No project" click', (done) => { + spyOn(this.mediator, 'setMoveToProjectId'); + + // Open the dropdown + this.$toggleButton.dropdown('toggle'); + + // Wait for the autocomplete request to finish + setTimeout(() => { + this.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click'); + + expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(0); + expect(this.$confirmButton.attr('disabled')).toBe('disabled'); + done(); + }, 0); + }); + + it('should set moveToProjectId on dropdown item click', (done) => { + spyOn(this.mediator, 'setMoveToProjectId'); + + // Open the dropdown + this.$toggleButton.dropdown('toggle'); + + // Wait for the autocomplete request to finish + setTimeout(() => { + this.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click'); + + expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(20); + expect(this.$confirmButton.attr('disabled')).toBe(undefined); + done(); + }, 0); + }); +}); diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js index 91a4dd669a7..a4bd8ba8d88 100644 --- a/spec/javascripts/sidebar/sidebar_service_spec.js +++ b/spec/javascripts/sidebar/sidebar_service_spec.js @@ -5,7 +5,11 @@ import Mock from './mock_data'; describe('Sidebar service', () => { beforeEach(() => { Vue.http.interceptors.push(Mock.sidebarMockInterceptor); - this.service = new SidebarService('/gitlab-org/gitlab-shell/issues/5.json'); + this.service = new SidebarService({ + endpoint: '/gitlab-org/gitlab-shell/issues/5.json', + moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move', + projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15', + }); }); afterEach(() => { @@ -19,7 +23,7 @@ describe('Sidebar service', () => { expect(resp).toBeDefined(); done(); }) - .catch(() => {}); + .catch(done.fail); }); it('updates the data', (done) => { @@ -28,6 +32,24 @@ describe('Sidebar service', () => { expect(resp).toBeDefined(); done(); }) - .catch(() => {}); + .catch(done.fail); + }); + + it('gets projects for autocomplete', (done) => { + this.service.getProjectsAutocomplete() + .then((resp) => { + expect(resp).toBeDefined(); + done(); + }) + .catch(done.fail); + }); + + it('moves the issue to another project', (done) => { + this.service.moveIssue(123) + .then((resp) => { + expect(resp).toBeDefined(); + done(); + }) + .catch(done.fail); }); }); diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js index b3fa156eb64..69eb3839d67 100644 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ b/spec/javascripts/sidebar/sidebar_store_spec.js @@ -82,4 +82,18 @@ describe('Sidebar store', () => { expect(this.store.humanTimeEstimate).toEqual(Mock.time.human_time_estimate); expect(this.store.humanTotalTimeSpent).toEqual(Mock.time.human_total_time_spent); }); + + it('set autocomplete projects', () => { + const projects = [{ id: 0 }]; + this.store.setAutocompleteProjects(projects); + + expect(this.store.autocompleteProjects).toEqual(projects); + }); + + it('set move to project ID', () => { + const projectId = 7; + this.store.setMoveToProjectId(projectId); + + expect(this.store.moveToProjectId).toEqual(projectId); + }); }); diff --git a/spec/javascripts/vue_shared/components/identicon_spec.js b/spec/javascripts/vue_shared/components/identicon_spec.js index 4f194e5a64e..647680f00f7 100644 --- a/spec/javascripts/vue_shared/components/identicon_spec.js +++ b/spec/javascripts/vue_shared/components/identicon_spec.js @@ -1,25 +1,30 @@ import Vue from 'vue'; import identiconComponent from '~/vue_shared/components/identicon.vue'; -const createComponent = () => { +const createComponent = (sizeClass) => { const Component = Vue.extend(identiconComponent); return new Component({ propsData: { entityId: 1, entityName: 'entity-name', + sizeClass, }, }).$mount(); }; describe('IdenticonComponent', () => { - let vm; + describe('computed', () => { + let vm; - beforeEach(() => { - vm = createComponent(); - }); + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); - describe('computed', () => { describe('identiconStyles', () => { it('should return styles attribute value with `background-color` property', () => { vm.entityId = 4; @@ -48,9 +53,20 @@ describe('IdenticonComponent', () => { describe('template', () => { it('should render identicon', () => { + const vm = createComponent(); + expect(vm.$el.nodeName).toBe('DIV'); expect(vm.$el.classList.contains('identicon')).toBeTruthy(); + expect(vm.$el.classList.contains('s40')).toBeTruthy(); expect(vm.$el.getAttribute('style').indexOf('background-color') > -1).toBeTruthy(); + vm.$destroy(); + }); + + it('should render identicon with provided sizing class', () => { + const vm = createComponent('s32'); + + expect(vm.$el.classList.contains('s32')).toBeTruthy(); + vm.$destroy(); }); }); }); diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb new file mode 100644 index 00000000000..049d025a5b9 --- /dev/null +++ b/spec/lib/banzai/commit_renderer_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Banzai::CommitRenderer do + describe '.render' do + it 'renders a commit description and title' do + user = double(:user) + project = create(:project, :repository) + + expect(Banzai::ObjectRenderer).to receive(:new).with(project, user).and_call_original + + described_class::ATTRIBUTES.each do |attr| + expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original + expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr) + end + + described_class.render([project.commit], project, user) + end + end +end diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index ff6b19459bb..85eddde732e 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -96,5 +96,41 @@ describe Banzai::Filter::TableOfContentsFilter do expect(links.last.attr('href')).to eq '#header-2' expect(links.last.text).to eq 'Header 2' end + + context 'table of contents nesting' do + let(:results) do + result( + header(1, 'Header 1') << + header(2, 'Header 1-1') << + header(3, 'Header 1-1-1') << + header(2, 'Header 1-2') << + header(1, 'Header 2') << + header(2, 'Header 2-1') + ) + end + + it 'keeps list levels regarding header levels' do + items = doc.css('li') + + # Header 1 + expect(items[0].ancestors).to satisfy_none { |node| node.name == 'li' } + + # Header 1-1 + expect(items[1].ancestors).to include(items[0]) + + # Header 1-1-1 + expect(items[2].ancestors).to include(items[0], items[1]) + + # Header 1-2 + expect(items[3].ancestors).to include(items[0]) + expect(items[3].ancestors).not_to include(items[1]) + + # Header 2 + expect(items[4].ancestors).to satisfy_none { |node| node.name == 'li' } + + # Header 2-1 + expect(items[5].ancestors).to include(items[4]) + end + end end end diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb index 7f5d481c36c..b172a1b718c 100644 --- a/spec/lib/banzai/object_renderer_spec.rb +++ b/spec/lib/banzai/object_renderer_spec.rb @@ -1,53 +1,77 @@ require 'spec_helper' describe Banzai::ObjectRenderer do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:renderer) { described_class.new(project, user, custom_value: 'value') } let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) } describe '#render' do - it 'renders and redacts an Array of objects' do - renderer.render([object], :note) + context 'with cache' do + it 'renders and redacts an Array of objects' do + renderer.render([object], :note) - expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>' - expect(object.user_visible_reference_count).to eq 0 - end + expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>' + expect(object.user_visible_reference_count).to eq 0 + end - it 'calls Banzai::Redactor to perform redaction' do - expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original + it 'calls Banzai::Redactor to perform redaction' do + expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original - renderer.render([object], :note) - end + renderer.render([object], :note) + end - it 'retrieves field content using Banzai.render_field' do - expect(Banzai).to receive(:render_field).with(object, :note).and_call_original + it 'retrieves field content using Banzai::Renderer.render_field' do + expect(Banzai::Renderer).to receive(:render_field).with(object, :note).and_call_original - renderer.render([object], :note) - end + renderer.render([object], :note) + end - it 'passes context to PostProcessPipeline' do - another_user = create(:user) - another_project = create(:project) - object = Note.new( - note: 'hello', - note_html: 'hello', - author: another_user, - project: another_project - ) - - expect(Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).with( - anything, - hash_including( - skip_redaction: true, - current_user: user, - project: another_project, + it 'passes context to PostProcessPipeline' do + another_user = create(:user) + another_project = create(:project) + object = Note.new( + note: 'hello', + note_html: 'hello', author: another_user, - custom_value: 'value' + project: another_project ) - ).and_call_original - renderer.render([object], :note) + expect(Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).with( + anything, + hash_including( + skip_redaction: true, + current_user: user, + project: another_project, + author: another_user, + custom_value: 'value' + ) + ).and_call_original + + renderer.render([object], :note) + end + end + + context 'without cache' do + let(:commit) { project.commit } + + it 'renders and redacts an Array of objects' do + renderer.render([commit], :title) + + expect(commit.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'") + end + + it 'calls Banzai::Redactor to perform redaction' do + expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original + + renderer.render([commit], :title) + end + + it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do + expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title).and_call_original + + renderer.render([commit], :title) + end end end end diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb index 0e094405e33..da42272bbef 100644 --- a/spec/lib/banzai/renderer_spec.rb +++ b/spec/lib/banzai/renderer_spec.rb @@ -4,6 +4,7 @@ describe Banzai::Renderer do def fake_object(fresh:) object = double('object') + allow(object).to receive(:respond_to?).with(:cached_markdown_fields).and_return(true) allow(object).to receive(:cached_html_up_to_date?).with(:field).and_return(fresh) allow(object).to receive(:cached_html_for).with(:field).and_return('field_html') @@ -12,25 +13,38 @@ describe Banzai::Renderer do describe '#render_field' do let(:renderer) { described_class } - subject { renderer.render_field(object, :field) } - context 'with a stale cache' do - let(:object) { fake_object(fresh: false) } + context 'without cache' do + let(:commit) { create(:project, :repository).commit } - it 'caches and returns the result' do - expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) + it 'returns cacheless render field' do + expect(renderer).to receive(:cacheless_render_field).with(commit, :title) - is_expected.to eq('field_html') + renderer.render_field(commit, :title) end end - context 'with an up-to-date cache' do - let(:object) { fake_object(fresh: true) } + context 'with cache' do + subject { renderer.render_field(object, :field) } - it 'uses the cache' do - expect(object).to receive(:refresh_markdown_cache!).never + context 'with a stale cache' do + let(:object) { fake_object(fresh: false) } - is_expected.to eq('field_html') + it 'caches and returns the result' do + expect(object).to receive(:refresh_markdown_cache!).with(do_update: true) + + is_expected.to eq('field_html') + end + end + + context 'with an up-to-date cache' do + let(:object) { fake_object(fresh: true) } + + it 'uses the cache' do + expect(object).to receive(:refresh_markdown_cache!).never + + is_expected.to eq('field_html') + end end end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index c70a4cb55fe..1efd3113a43 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -164,9 +164,46 @@ module Ci expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' end end + + context 'when kubernetes policy is specified' do + let(:pipeline) { create(:ci_empty_pipeline) } + + let(:config) do + YAML.dump( + spinach: { stage: 'test', script: 'spinach' }, + production: { + stage: 'deploy', + script: 'cap', + only: { kubernetes: 'active' } + } + ) + end + + context 'when kubernetes is active' do + let(:project) { create(:kubernetes_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + it 'returns seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when kubernetes is not active' do + it 'does not return seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + end end - describe "#builds_for_ref" do + describe "#builds_for_stage_and_ref" do let(:type) { 'test' } it "returns builds if no branch specified" do diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index 0d5fffa38ff..c56b08b18a2 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -214,7 +214,7 @@ end # The background migration relies on a temporary table, hence we're migrating # to a specific version of the database where said table is still present. # -describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170608152748 do +describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do let(:migration) { described_class.new } let(:project) { create(:project_empty_repo) } let(:author) { create(:user) } diff --git a/spec/lib/gitlab/ci/build/artifacts/path_spec.rb b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb new file mode 100644 index 00000000000..7bd6a2ead25 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::Path do + describe '#valid?' do + context 'when path contains a zero character' do + it 'is not valid' do + expect(described_class.new("something/\255")).not_to be_valid + end + end + + context 'when path is not utf8 string' do + it 'is not valid' do + expect(described_class.new("something/\0")).not_to be_valid + end + end + + context 'when path is valid' do + it 'is valid' do + expect(described_class.new("some/file/path")).to be_valid + end + end + end + + describe '#directory?' do + context 'when path ends with a directory indicator' do + it 'is a directory' do + expect(described_class.new("some/file/dir/")).to be_directory + end + end + + context 'when path does not end with a directory indicator' do + it 'is not a directory' do + expect(described_class.new("some/file")).not_to be_directory + end + end + end + + describe '#name' do + it 'returns a base name' do + expect(described_class.new("some/file").name).to eq 'file' + end + end + + describe '#nodes' do + it 'returns number of path nodes' do + expect(described_class.new("some/dir/file").nodes).to eq 2 + end + end + + describe '#to_s' do + context 'when path is valid' do + it 'returns a string representation of a path' do + expect(described_class.new('some/path').to_s).to eq 'some/path' + end + end + + context 'when path is invalid' do + it 'raises an error' do + expect { described_class.new("invalid/\0").to_s } + .to raise_error ArgumentError + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 36a84da4a52..5e83abf645b 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -16,8 +16,8 @@ describe Gitlab::Ci::Config::Entry::Policy do end describe '#value' do - it 'returns key value' do - expect(entry.value).to eq config + it 'returns refs hash' do + expect(entry.value).to eq(refs: config) end end end @@ -56,6 +56,50 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + context 'when using complex policy' do + context 'when specifiying refs policy' do + let(:config) { { refs: ['master'] } } + + it 'is a correct configuraton' do + expect(entry).to be_valid + expect(entry.value).to eq(refs: %w[master]) + end + end + + context 'when specifying kubernetes policy' do + let(:config) { { kubernetes: 'active' } } + + it 'is a correct configuraton' do + expect(entry).to be_valid + expect(entry.value).to eq(kubernetes: 'active') + end + end + + context 'when specifying invalid kubernetes policy' do + let(:config) { { kubernetes: 'something' } } + + it 'reports an error about invalid policy' do + expect(entry.errors).to include /unknown value: something/ + end + end + + context 'when specifying unknown policy' do + let(:config) { { refs: ['master'], invalid: :something } } + + it 'returns error about invalid key' do + expect(entry.errors).to include /unknown keys: invalid/ + end + end + + context 'when policy is empty' do + let(:config) { {} } + + it 'is not a valid configuration' do + expect(entry.errors).to include /can't be blank/ + end + end + end + context 'when policy strategy does not match' do let(:config) { 'string strategy' } diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb index d7e91a5a62c..9ecd128faca 100644 --- a/spec/lib/gitlab/ci/stage/seed_spec.rb +++ b/spec/lib/gitlab/ci/stage/seed_spec.rb @@ -27,6 +27,26 @@ describe Gitlab::Ci::Stage::Seed do expect(subject.builds) .to all(include(trigger_request: pipeline.trigger_requests.first)) end + + context 'when a ref is protected' do + before do + allow_any_instance_of(Project).to receive(:protected_for?).and_return(true) + end + + it 'returns protected builds' do + expect(subject.builds).to all(include(protected: true)) + end + end + + context 'when a ref is unprotected' do + before do + allow_any_instance_of(Project).to receive(:protected_for?).and_return(false) + end + + it 'returns unprotected builds' do + expect(subject.builds).to all(include(protected: false)) + end + end end describe '#user=' do diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index dfbdbee48f7..d39b33a0c05 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -273,6 +273,25 @@ EOT end end + describe '#json_safe_diff' do + let(:project) { create(:project, :repository) } + + it 'fake binary message when it detects binary' do + # Rugged will not detect this as binary, but we can fake it + diff_message = "Binary files files/images/icn-time-tracking.pdf and files/images/icn-time-tracking.pdf differ\n" + binary_diff = described_class.between(project.repository, 'add-pdf-text-binary', 'add-pdf-text-binary^').first + + expect(binary_diff.diff).not_to be_empty + expect(binary_diff.json_safe_diff).to eq(diff_message) + end + + it 'leave non-binary diffs as-is' do + diff = described_class.new(@rugged_diff) + + expect(diff.json_safe_diff).to eq(diff.diff) + end + end + describe '#submodule?' do before do commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e') diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 4cfb4b7d357..556a148c3bc 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -390,46 +390,73 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#delete_branch" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.delete_branch("feature") + shared_examples "deleting a branch" do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } + + after do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end + + it "removes the branch from the repo" do + branch_name = "to-be-deleted-soon" + + repository.create_branch(branch_name) + expect(repository.rugged.branches[branch_name]).not_to be_nil + + repository.delete_branch(branch_name) + expect(repository.rugged.branches[branch_name]).to be_nil + end + + context "when branch does not exist" do + it "raises a DeleteBranchError exception" do + expect { repository.delete_branch("this-branch-does-not-exist") }.to raise_error(Gitlab::Git::Repository::DeleteBranchError) + end + end end - it "should remove the branch from the repo" do - expect(@repo.rugged.branches["feature"]).to be_nil + context "when Gitaly delete_branch is enabled" do + it_behaves_like "deleting a branch" end - after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) - ensure_seeds + context "when Gitaly delete_branch is disabled", skip_gitaly_mock: true do + it_behaves_like "deleting a branch" end end describe "#create_branch" do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - end + shared_examples 'creating a branch' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - it "should create a new branch" do - expect(@repo.create_branch('new_branch', 'master')).not_to be_nil - end + after do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end - it "should create a new branch with the right name" do - expect(@repo.create_branch('another_branch', 'master').name).to eq('another_branch') - end + it "should create a new branch" do + expect(repository.create_branch('new_branch', 'master')).not_to be_nil + end - it "should fail if we create an existing branch" do - @repo.create_branch('duplicated_branch', 'master') - expect {@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") + it "should create a new branch with the right name" do + expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch') + end + + it "should fail if we create an existing branch" do + repository.create_branch('duplicated_branch', 'master') + expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") + end + + it "should fail if we create a branch from a non existing ref" do + expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") + end end - it "should fail if we create a branch from a non existing ref" do - expect {@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") + context 'when Gitaly create_branch feature is enabled' do + it_behaves_like 'creating a branch' end - after(:all) do - FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) - ensure_seeds + context 'when Gitaly create_branch feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'creating a branch' end end @@ -905,7 +932,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'should set the autocrlf option to the provided option' do @repo.autocrlf = :input - File.open(File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file| + File.open(File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH, 'config')) do |config_file| expect(config_file.read).to match('autocrlf = input') end end @@ -916,27 +943,37 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#find_branch' do - it 'should return a Branch for master' do - branch = repository.find_branch('master') + shared_examples 'finding a branch' do + it 'should return a Branch for master' do + branch = repository.find_branch('master') - expect(branch).to be_a_kind_of(Gitlab::Git::Branch) - expect(branch.name).to eq('master') - end + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end + + it 'should handle non-existent branch' do + branch = repository.find_branch('this-is-garbage') - it 'should handle non-existent branch' do - branch = repository.find_branch('this-is-garbage') + expect(branch).to eq(nil) + end + end - expect(branch).to eq(nil) + context 'when Gitaly find_branch feature is enabled' do + it_behaves_like 'finding a branch' end - it 'should reload Rugged::Repository and return master' do - expect(Rugged::Repository).to receive(:new).twice.and_call_original + context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'finding a branch' + + it 'should reload Rugged::Repository and return master' do + expect(Rugged::Repository).to receive(:new).twice.and_call_original - repository.find_branch('master') - branch = repository.find_branch('master', force_reload: true) + repository.find_branch('master') + branch = repository.find_branch('master', force_reload: true) - expect(branch).to be_a_kind_of(Gitlab::Git::Branch) - expect(branch.name).to eq('master') + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end end end @@ -967,7 +1004,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with local and remote branches' do let(:repository) do - Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'), '') + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end before do @@ -1014,7 +1051,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with local and remote branches' do let(:repository) do - Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'), '') + Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end before do @@ -1220,7 +1257,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#local_branches' do before(:all) do - @repo = Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'), '') + @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') end after(:all) do diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index e521fcc6dc1..b07462e4978 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -2,45 +2,9 @@ require 'rails_helper' describe Gitlab::Gpg::Commit do describe '#signature' do - let!(:project) { create :project, :repository, path: 'sample-project' } - let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - - context 'unsigned commit' do - it 'returns nil' do - expect(described_class.new(project, commit_sha).signature).to be_nil - end - end - - context 'known and verified public key' do - let!(:gpg_key) do - create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first) - end - - before do - allow(Rugged::Commit).to receive(:extract_signature) - .with(Rugged::Repository, commit_sha) - .and_return( - [ - GpgHelpers::User1.signed_commit_signature, - GpgHelpers::User1.signed_commit_base_data - ] - ) - end - - it 'returns a valid signature' do - expect(described_class.new(project, commit_sha).signature).to have_attributes( - commit_sha: commit_sha, - project: project, - gpg_key: gpg_key, - gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - gpg_key_user_name: GpgHelpers::User1.names.first, - gpg_key_user_email: GpgHelpers::User1.emails.first, - valid_signature: true - ) - end - + shared_examples 'returns the cached signature on second call' do it 'returns the cached signature on second call' do - gpg_commit = described_class.new(project, commit_sha) + gpg_commit = described_class.new(commit) expect(gpg_commit).to receive(:using_keychain).and_call_original gpg_commit.signature @@ -51,11 +15,140 @@ describe Gitlab::Gpg::Commit do end end - context 'known but unverified public key' do - let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key } + let!(:project) { create :project, :repository, path: 'sample-project' } + let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - before do - allow(Rugged::Commit).to receive(:extract_signature) + context 'unsigned commit' do + let!(:commit) { create :commit, project: project, sha: commit_sha } + + it 'returns nil' do + expect(described_class.new(commit).signature).to be_nil + end + end + + context 'known key' do + context 'user matches the key uid' do + context 'user email matches the email committer' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first } + + let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) } + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns a valid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'verified' + ) + end + + it_behaves_like 'returns the cached signature on second call' + end + + context 'user email does not match the committer email, but is the same user' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first } + + let(:user) do + create(:user, email: GpgHelpers::User1.emails.first).tap do |user| + create :email, user: user, email: GpgHelpers::User2.emails.first + end + end + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns an invalid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'same_user_different_email' + ) + end + + it_behaves_like 'returns the cached signature on second call' + end + + context 'user email does not match the committer email' do + let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first } + + let(:user) { create(:user, email: GpgHelpers::User1.emails.first) } + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) + .with(Rugged::Repository, commit_sha) + .and_return( + [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ] + ) + end + + it 'returns an invalid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'other_user' + ) + end + + it_behaves_like 'returns the cached signature on second call' + end + end + + context 'user does not match the key uid' do + let!(:commit) { create :commit, project: project, sha: commit_sha } + + let(:user) { create(:user, email: GpgHelpers::User2.emails.first) } + + let!(:gpg_key) do + create :gpg_key, key: GpgHelpers::User1.public_key, user: user + end + + before do + allow(Rugged::Commit).to receive(:extract_signature) .with(Rugged::Repository, commit_sha) .and_return( [ @@ -63,33 +156,27 @@ describe Gitlab::Gpg::Commit do GpgHelpers::User1.signed_commit_base_data ] ) - end - - it 'returns an invalid signature' do - expect(described_class.new(project, commit_sha).signature).to have_attributes( - commit_sha: commit_sha, - project: project, - gpg_key: gpg_key, - gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - gpg_key_user_name: GpgHelpers::User1.names.first, - gpg_key_user_email: GpgHelpers::User1.emails.first, - valid_signature: false - ) - end - - it 'returns the cached signature on second call' do - gpg_commit = described_class.new(project, commit_sha) - - expect(gpg_commit).to receive(:using_keychain).and_call_original - gpg_commit.signature + end + + it 'returns an invalid signature' do + expect(described_class.new(commit).signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'unverified_key' + ) + end - # consecutive call - expect(gpg_commit).not_to receive(:using_keychain).and_call_original - gpg_commit.signature + it_behaves_like 'returns the cached signature on second call' end end - context 'unknown public key' do + context 'unknown key' do + let!(:commit) { create :commit, project: project, sha: commit_sha } + before do allow(Rugged::Commit).to receive(:extract_signature) .with(Rugged::Repository, commit_sha) @@ -102,27 +189,18 @@ describe Gitlab::Gpg::Commit do end it 'returns an invalid signature' do - expect(described_class.new(project, commit_sha).signature).to have_attributes( + expect(described_class.new(commit).signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, gpg_key_user_name: nil, gpg_key_user_email: nil, - valid_signature: false + verification_status: 'unknown_key' ) end - it 'returns the cached signature on second call' do - gpg_commit = described_class.new(project, commit_sha) - - expect(gpg_commit).to receive(:using_keychain).and_call_original - gpg_commit.signature - - # consecutive call - expect(gpg_commit).not_to receive(:using_keychain).and_call_original - gpg_commit.signature - end + it_behaves_like 'returns the cached signature on second call' end end end 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 4de4419de27..b9fd4d02156 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -4,8 +4,29 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do describe '#run' do let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } let!(:project) { create :project, :repository, path: 'sample-project' } + let!(:raw_commit) do + raw_commit = double( + :raw_commit, + signature: [ + GpgHelpers::User1.signed_commit_signature, + GpgHelpers::User1.signed_commit_base_data + ], + sha: commit_sha, + committer_email: GpgHelpers::User1.emails.first + ) + + allow(raw_commit).to receive :save! + + raw_commit + end + + let!(:commit) do + create :commit, git_commit: raw_commit, project: project + end before do + allow_any_instance_of(Project).to receive(:commit).and_return(commit) + allow(Rugged::Commit).to receive(:extract_signature) .with(Rugged::Repository, commit_sha) .and_return( @@ -25,7 +46,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' end it 'assigns the gpg key to the signature when the missing gpg key is added' do @@ -39,7 +60,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' ) end @@ -54,7 +75,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' ) end end @@ -68,7 +89,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unknown_key' end it 'updates the signature to being valid when the missing gpg key is added' do @@ -82,7 +103,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' ) end @@ -97,7 +118,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unknown_key' ) end end @@ -115,7 +136,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: nil, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unknown_key' end it 'updates the signature to being valid when the user updates the email address' do @@ -123,7 +144,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do key: GpgHelpers::User1.public_key, user: user - expect(invalid_gpg_signature.reload.valid_signature).to be_falsey + expect(invalid_gpg_signature.reload.verification_status).to eq 'unverified_key' # InvalidGpgSignatureUpdater is called by the after_update hook user.update_attributes!(email: GpgHelpers::User1.emails.first) @@ -133,7 +154,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: true + verification_status: 'verified' ) end @@ -147,7 +168,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unverified_key' ) # InvalidGpgSignatureUpdater is called by the after_update hook @@ -158,7 +179,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do commit_sha: commit_sha, gpg_key: gpg_key, gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, - valid_signature: false + verification_status: 'unverified_key' ) end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index 30ad033b204..11a2aea1915 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -42,6 +42,21 @@ describe Gitlab::Gpg do described_class.user_infos_from_key('bogus') ).to eq [] end + + it 'downcases the email' do + public_key = double(:key) + fingerprints = double(:fingerprints) + uid = double(:uid, name: 'Nannie Bernhard', email: 'NANNIE.BERNHARD@EXAMPLE.COM') + raw_key = double(:raw_key, uids: [uid]) + allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints) + allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key]) + + user_infos = described_class.user_infos_from_key(public_key) + expect(user_infos).to eq([{ + name: 'Nannie Bernhard', + email: 'nannie.bernhard@example.com' + }]) + end end describe '.current_home_dir' do diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb index cd5c2b99751..3a962ba7f22 100644 --- a/spec/lib/gitlab/i18n/po_linter_spec.rb +++ b/spec/lib/gitlab/i18n/po_linter_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'simple_po_parser' describe Gitlab::I18n::PoLinter do let(:linter) { described_class.new(po_path) } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index dbe042adff5..3fb8edeb701 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -265,6 +265,7 @@ project: - statistics - container_repositories - uploads +- members_and_requesters award_emoji: - awardable - user 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 5b16fc5d084..d664d371028 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -11,8 +11,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') @project = create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') - allow(@project.repository).to receive(:fetch_ref).and_return(true) - allow(@project.repository.raw).to receive(:rugged_branch_exists?).and_return(false) + allow_any_instance_of(Repository).to receive(:fetch_ref).and_return(true) + allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false) expect_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch).with('feature', 'DCBA') allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch) 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 065b0ec6658..8e3554375e8 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -117,6 +117,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1) end + it 'has no when YML attributes but only the DB column' do + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))) + expect_any_instance_of(Ci::GitlabCiYamlProcessor).not_to receive(:build_attributes) + + saved_project_json + end + it 'has pipeline commits' do expect(saved_project_json['pipelines']).not_to be_empty end @@ -251,15 +258,11 @@ describe Gitlab::ImportExport::ProjectTreeSaver do create(:label_priority, label: group_label, priority: 1) milestone = create(:milestone, project: project) merge_request = create(:merge_request, source_project: project, milestone: milestone) - commit_status = create(:commit_status, project: project) - ci_pipeline = create(:ci_pipeline, - project: project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch, - statuses: [commit_status]) + ci_build = create(:ci_build, project: project, when: nil) + ci_build.pipeline.update(project: project) + create(:commit_status, project: project, pipeline: ci_build.pipeline) - create(:ci_build, pipeline: ci_pipeline, project: project) create(:milestone, project: project) create(:note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) @@ -267,7 +270,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do create(:note_on_commit, author: user, project: project, - commit_id: ci_pipeline.sha) + commit_id: ci_build.pipeline.sha) create(:event, :created, target: milestone, project: project, author: user) create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index f2224bc3aa6..6503d8fb0ac 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -65,6 +65,7 @@ Note: - change_position - resolved_at - resolved_by_id +- resolved_by_push - discussion_id - original_discussion_id LabelLink: @@ -225,6 +226,7 @@ Ci::Pipeline: - auto_canceled_by_id - pipeline_schedule_id - config_source +- protected Ci::Stage: - id - name @@ -277,6 +279,8 @@ CommitStatus: - coverage_regex - auto_canceled_by_id - retried +- protected +- failure_reason Ci::Variable: - id - project_id @@ -396,6 +400,7 @@ Project: - public_builds - last_repository_check_failed - last_repository_check_at +- collapse_outdated_diff_comments - container_registry_enabled - only_allow_merge_if_pipeline_succeeds - has_external_issue_tracker @@ -404,6 +409,7 @@ Project: - only_allow_merge_if_all_discussions_are_resolved - auto_cancel_pending_pipelines - printing_merge_request_link_enabled +- resolve_outdated_diff_discussions - build_allow_git_fetch - last_repository_updated_at - ci_config_path diff --git a/spec/lib/gitlab/issuables_count_for_state_spec.rb b/spec/lib/gitlab/issuables_count_for_state_spec.rb new file mode 100644 index 00000000000..c262fdfcb61 --- /dev/null +++ b/spec/lib/gitlab/issuables_count_for_state_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::IssuablesCountForState do + let(:finder) do + double(:finder, count_by_state: { opened: 2, closed: 1 }) + end + + let(:counter) { described_class.new(finder) } + + describe '#for_state_or_opened' do + it 'returns the number of issuables for the given state' do + expect(counter.for_state_or_opened(:closed)).to eq(1) + end + + it 'returns the number of open issuables when no state is given' do + expect(counter.for_state_or_opened).to eq(2) + end + + it 'returns the number of open issuables when a nil value is given' do + expect(counter.for_state_or_opened(nil)).to eq(2) + end + end + + describe '#[]' do + it 'returns the number of issuables for the given state' do + expect(counter[:closed]).to eq(1) + end + + it 'casts valid states from Strings to Symbols' do + expect(counter['closed']).to eq(1) + end + + it 'returns 0 when using an invalid state name as a String' do + expect(counter['kittens']).to be_zero + end + end +end diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 5100a5a609e..6a6e465cea2 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -37,7 +37,8 @@ describe Gitlab::LDAP::User do end it "does not mark existing ldap user as changed" do - create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', external_email: true, email_provider: 'ldapmain') + create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') + ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true) expect(ldap_user.changed?).to be_falsey end end @@ -141,12 +142,12 @@ describe Gitlab::LDAP::User do expect(ldap_user.gl_user.email).to eq(info[:email]) end - it "has external_email set to true" do - expect(ldap_user.gl_user.external_email?).to be(true) + it "has user_synced_attributes_metadata email set to true" do + expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_truthy end - it "has email_provider set to provider" do - expect(ldap_user.gl_user.email_provider).to eql 'ldapmain' + it "has synced_attribute_provider set to ldapmain" do + expect(ldap_user.gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain' end end @@ -156,11 +157,11 @@ describe Gitlab::LDAP::User do end it "has a temp email" do - expect(ldap_user.gl_user.temp_oauth_email?).to be(true) + expect(ldap_user.gl_user.temp_oauth_email?).to be_truthy end - it "has external_email set to false" do - expect(ldap_user.gl_user.external_email?).to be(false) + it "has synced attribute email set to false" do + expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_falsey end end end @@ -168,7 +169,7 @@ describe Gitlab::LDAP::User do describe 'blocking' do def configure_block(value) allow_any_instance_of(Gitlab::LDAP::Config) - .to receive(:block_auto_created_users).and_return(value) + .to receive(:block_auto_created_users).and_return(value) end context 'signup' do diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 2cf0f7516de..8aaf320cbf5 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -10,7 +10,11 @@ describe Gitlab::OAuth::User do { nickname: '-john+gitlab-ETC%.git@gmail.com', name: 'John', - email: 'john@mail.com' + email: 'john@mail.com', + address: { + locality: 'locality', + country: 'country' + } } end let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } @@ -422,11 +426,12 @@ describe Gitlab::OAuth::User do end end - describe 'updating email' do + describe 'ensure backwards compatibility with with sync email from provider option' do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } before do stub_omniauth_config(sync_email_from_provider: 'my-provider') + stub_omniauth_config(sync_profile_from_provider: ['my-provider']) end context "when provider sets an email" do @@ -434,12 +439,12 @@ describe Gitlab::OAuth::User do expect(gl_user.email).to eq(info_hash[:email]) end - it "has external_email set to true" do - expect(gl_user.external_email?).to be(true) + it "has external_attributes set to true" do + expect(gl_user.user_synced_attributes_metadata).not_to be_nil end - it "has email_provider set to provider" do - expect(gl_user.email_provider).to eql 'my-provider' + it "has attributes_provider set to my-provider" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' end end @@ -452,8 +457,9 @@ describe Gitlab::OAuth::User do expect(gl_user.email).not_to eq(info_hash[:email]) end - it "has external_email set to false" do - expect(gl_user.external_email?).to be(false) + it "has user_synced_attributes_metadata set to nil" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' + expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey end end end @@ -487,4 +493,172 @@ describe Gitlab::OAuth::User do end end end + + describe 'updating email with sync profile' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + before do + stub_omniauth_config(sync_profile_from_provider: ['my-provider']) + stub_omniauth_config(sync_profile_attributes: true) + end + + context "when provider sets an email" do + it "updates the user email" do + expect(gl_user.email).to eq(info_hash[:email]) + end + + it "has email_synced_attribute set to true" do + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true) + end + + it "has my-provider as attributes_provider" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' + end + end + + context "when provider doesn't set an email" do + before do + info_hash.delete(:email) + end + + it "does not update the user email" do + expect(gl_user.email).not_to eq(info_hash[:email]) + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false) + end + end + end + + describe 'updating name' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + stub_omniauth_setting(sync_profile_attributes: true) + end + + context "when provider sets a name" do + it "updates the user name" do + expect(gl_user.name).to eq(info_hash[:name]) + end + end + + context "when provider doesn't set a name" do + before do + info_hash.delete(:name) + end + + it "does not update the user name" do + expect(gl_user.name).not_to eq(info_hash[:name]) + expect(gl_user.user_synced_attributes_metadata.name_synced).to be(false) + end + end + end + + describe 'updating location' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + stub_omniauth_setting(sync_profile_attributes: true) + end + + context "when provider sets a location" do + it "updates the user location" do + expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country]) + expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true) + end + end + + context "when provider doesn't set a location" do + before do + info_hash[:address].delete(:country) + info_hash[:address].delete(:locality) + end + + it "does not update the user location" do + expect(gl_user.location).to be_nil + expect(gl_user.user_synced_attributes_metadata.location_synced).to be(false) + end + end + end + + describe 'updating user info' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + context "update all info" do + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + stub_omniauth_setting(sync_profile_attributes: true) + end + + it "updates the user email" do + expect(gl_user.email).to eq(info_hash[:email]) + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true) + end + + it "updates the user name" do + expect(gl_user.name).to eq(info_hash[:name]) + expect(gl_user.user_synced_attributes_metadata.name_synced).to be(true) + end + + it "updates the user location" do + expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country]) + expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true) + end + + it "sets my-provider as the attributes provider" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql('my-provider') + end + end + + context "update only requested info" do + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + stub_omniauth_setting(sync_profile_attributes: %w(name location)) + end + + it "updates the user name" do + expect(gl_user.name).to eq(info_hash[:name]) + expect(gl_user.user_synced_attributes_metadata.name_synced).to be(true) + end + + it "updates the user location" do + expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country]) + expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true) + end + + it "does not update the user email" do + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false) + end + end + + context "update default_scope" do + before do + stub_omniauth_setting(sync_profile_from_provider: ['my-provider']) + end + + it "updates the user email" do + expect(gl_user.email).to eq(info_hash[:email]) + expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true) + end + end + + context "update no info when profile sync is nil" do + it "does not have sync_attribute" do + expect(gl_user.user_synced_attributes_metadata).to be(nil) + end + + it "does not update the user email" do + expect(gl_user.email).not_to eq(info_hash[:email]) + end + + it "does not update the user name" do + expect(gl_user.name).not_to eq(info_hash[:name]) + end + + it "does not update the user location" do + expect(gl_user.location).not_to eq(info_hash[:address][:country]) + end + end + end end diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb index 9d7b2136dab..48d56628ed5 100644 --- a/spec/lib/gitlab/sql/pattern_spec.rb +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -52,4 +52,124 @@ describe Gitlab::SQL::Pattern do end end end + + describe '.select_fuzzy_words' do + subject(:select_fuzzy_words) { Issue.select_fuzzy_words(query) } + + context 'with a word equal to 3 chars' do + let(:query) { 'foo' } + + it 'returns array cotaining a word' do + expect(select_fuzzy_words).to match_array(['foo']) + end + end + + context 'with a word shorter than 3 chars' do + let(:query) { 'fo' } + + it 'returns empty array' do + expect(select_fuzzy_words).to match_array([]) + end + end + + context 'with two words both equal to 3 chars' do + let(:query) { 'foo baz' } + + it 'returns array containing two words' do + expect(select_fuzzy_words).to match_array(%w[foo baz]) + end + end + + context 'with two words divided by two spaces both equal to 3 chars' do + let(:query) { 'foo baz' } + + it 'returns array containing two words' do + expect(select_fuzzy_words).to match_array(%w[foo baz]) + end + end + + context 'with two words equal to 3 chars and shorter than 3 chars' do + let(:query) { 'foo ba' } + + it 'returns array containing a word' do + expect(select_fuzzy_words).to match_array(['foo']) + end + end + + context 'with a multi-word surrounded by double quote' do + let(:query) { '"really bar"' } + + it 'returns array containing a multi-word' do + expect(select_fuzzy_words).to match_array(['really bar']) + end + end + + context 'with a multi-word surrounded by double quote and two words' do + let(:query) { 'foo "really bar" baz' } + + it 'returns array containing a multi-word and tow words' do + expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz']) + end + end + + context 'with a multi-word surrounded by double quote missing a spece before the first double quote' do + let(:query) { 'foo"really bar"' } + + it 'returns array containing two words with double quote' do + expect(select_fuzzy_words).to match_array(['foo"really', 'bar"']) + end + end + + context 'with a multi-word surrounded by double quote missing a spece after the second double quote' do + let(:query) { '"really bar"baz' } + + it 'returns array containing two words with double quote' do + expect(select_fuzzy_words).to match_array(['"really', 'bar"baz']) + end + end + + context 'with two multi-word surrounded by double quote and two words' do + let(:query) { 'foo "really bar" baz "awesome feature"' } + + it 'returns array containing two multi-words and tow words' do + expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz', 'awesome feature']) + end + end + end + + describe '.to_fuzzy_arel' do + subject(:to_fuzzy_arel) { Issue.to_fuzzy_arel(:title, query) } + + context 'with a word equal to 3 chars' do + let(:query) { 'foo' } + + it 'returns a single ILIKE condition' do + expect(to_fuzzy_arel.to_sql).to match(/title.*I?LIKE '\%foo\%'/) + end + end + + context 'with a word shorter than 3 chars' do + let(:query) { 'fo' } + + it 'returns nil' do + expect(to_fuzzy_arel).to be_nil + end + end + + context 'with two words both equal to 3 chars' do + let(:query) { 'foo baz' } + + it 'returns a joining LIKE condition using a AND' do + expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/) + end + end + + context 'with a multi-word surrounded by double quote and two words' do + let(:query) { 'foo "really bar" baz' } + + it 'returns a joining LIKE condition using a AND' do + expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/) + end + end + end end diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb index 6541326d1de..e2fa76522bc 100644 --- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb +++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb @@ -30,6 +30,14 @@ describe Gitlab::Template::GitlabCiYmlTemplate do end end + describe '.by_category' do + it 'returns sorted results' do + result = described_class.by_category('General') + + expect(result).to eq(result.sort) + end + end + describe '#content' do it 'loads the full file' do gitignore = subject.new(Rails.root.join('vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml')) @@ -38,4 +46,14 @@ describe Gitlab::Template::GitlabCiYmlTemplate do expect(gitignore.content).to start_with('#') end end + + describe '#<=>' do + it 'sorts lexicographically' do + one = described_class.new('a.gitlab-ci.yml') + other = described_class.new('z.gitlab-ci.yml') + + expect(one.<=>(other)).to be(-1) + expect([other, one].sort).to eq([one, other]) + end + end end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 308b1a128be..fdc3990132a 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -1,11 +1,7 @@ require 'spec_helper' describe Gitlab::UrlSanitizer do - let(:credentials) { { user: 'blah', password: 'password' } } - let(:url_sanitizer) do - described_class.new("https://github.com/me/project.git", credentials: credentials) - end - let(:user) { double(:user, username: 'john.doe') } + using RSpec::Parameterized::TableSyntax describe '.sanitize' do def sanitize_url(url) @@ -16,83 +12,166 @@ describe Gitlab::UrlSanitizer do }) end - it 'mask the credentials from HTTP URLs' do - filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/') + where(:input, :output) do + 'http://user:pass@test.com/root/repoC.git/' | 'http://*****:*****@test.com/root/repoC.git/' + 'https://user:pass@test.com/root/repoA.git/' | 'https://*****:*****@test.com/root/repoA.git/' + 'ssh://user@host.test/path/to/repo.git' | 'ssh://*****@host.test/path/to/repo.git' - expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/") - end + # git protocol does not support authentication but clean any details anyway + 'git://user:pass@host.test/path/to/repo.git' | 'git://*****:*****@host.test/path/to/repo.git' + 'git://host.test/path/to/repo.git' | 'git://host.test/path/to/repo.git' - it 'mask the credentials from HTTPS URLs' do - filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/') + # SCP-style URLs are left unmodified + 'user@server:project.git' | 'user@server:project.git' + 'user:pass@server:project.git' | 'user:pass@server:project.git' - expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/") + # return an empty string for invalid URLs + 'ssh://' | '' end - it 'mask credentials from SSH URLs' do - filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git') - - expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git") + with_them do + it { expect(sanitize_url(input)).to include("repository '#{output}' not found") } end + end - it 'does not modify Git URLs' do - # git protocol does not support authentication - filtered_content = sanitize_url('git://host.test/path/to/repo.git') + describe '.valid?' do + where(:value, :url) do + false | nil + false | '' + false | '123://invalid:url' + true | 'valid@project:url.git' + true | 'ssh://example.com' + true | 'ssh://:@example.com' + true | 'ssh://foo@example.com' + true | 'ssh://foo:bar@example.com' + true | 'ssh://foo:bar@example.com/group/group/project.git' + true | 'git://example.com/group/group/project.git' + true | 'git://foo:bar@example.com/group/group/project.git' + true | 'http://foo:bar@example.com/group/group/project.git' + true | 'https://foo:bar@example.com/group/group/project.git' + end - expect(filtered_content).to include("git://host.test/path/to/repo.git") + with_them do + it { expect(described_class.valid?(url)).to eq(value) } end + end + + describe '#sanitized_url' do + context 'credentials in hash' do + where(username: ['foo', '', nil], password: ['bar', '', nil]) - it 'does not modify scp-like URLs' do - filtered_content = sanitize_url('user@server:project.git') + with_them do + let(:credentials) { { user: username, password: password } } + subject { described_class.new('http://example.com', credentials: credentials).sanitized_url } - expect(filtered_content).to include("user@server:project.git") + it { is_expected.to eq('http://example.com') } + end end - it 'returns an empty string for invalid URLs' do - filtered_content = sanitize_url('ssh://') + context 'credentials in URL' do + where(userinfo: %w[foo:bar@ foo@ :bar@ :@ @] + [nil]) - expect(filtered_content).to include("repository '' not found") - end - end + with_them do + subject { described_class.new("http://#{userinfo}example.com").sanitized_url } - describe '.valid?' do - it 'validates url strings' do - expect(described_class.valid?(nil)).to be(false) - expect(described_class.valid?('valid@project:url.git')).to be(true) - expect(described_class.valid?('123://invalid:url')).to be(false) + it { is_expected.to eq('http://example.com') } + end end end - describe '#sanitized_url' do - it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") } - end - describe '#credentials' do - it { expect(url_sanitizer.credentials).to eq(credentials) } + context 'credentials in hash' do + where(:input, :output) do + { user: 'foo', password: 'bar' } | { user: 'foo', password: 'bar' } + { user: 'foo', password: '' } | { user: 'foo', password: nil } + { user: 'foo', password: nil } | { user: 'foo', password: nil } + { user: '', password: 'bar' } | { user: nil, password: 'bar' } + { user: '', password: '' } | { user: nil, password: nil } + { user: '', password: nil } | { user: nil, password: nil } + { user: nil, password: 'bar' } | { user: nil, password: 'bar' } + { user: nil, password: '' } | { user: nil, password: nil } + { user: nil, password: nil } | { user: nil, password: nil } + end - context 'when user is given to #initialize' do - let(:url_sanitizer) do - described_class.new("https://github.com/me/project.git", credentials: { user: user.username }) + with_them do + subject { described_class.new('user@example.com:path.git', credentials: input).credentials } + + it { is_expected.to eq(output) } end - it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) } + it 'overrides URL-provided credentials' do + sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' }) + + expect(sanitizer.credentials).to eq(user: 'c', password: 'd') + end + end + + context 'credentials in URL' do + where(:url, :credentials) do + 'http://foo:bar@example.com' | { user: 'foo', password: 'bar' } + 'http://:bar@example.com' | { user: nil, password: 'bar' } + 'http://foo:@example.com' | { user: 'foo', password: nil } + 'http://foo@example.com' | { user: 'foo', password: nil } + 'http://:@example.com' | { user: nil, password: nil } + 'http://@example.com' | { user: nil, password: nil } + 'http://example.com' | { user: nil, password: nil } + + # Credentials from SCP-style URLs are not supported at present + 'foo@example.com:path' | { user: nil, password: nil } + 'foo:bar@example.com:path' | { user: nil, password: nil } + + # Other invalid URLs + nil | { user: nil, password: nil } + '' | { user: nil, password: nil } + 'no' | { user: nil, password: nil } + end + + with_them do + subject { described_class.new(url).credentials } + + it { is_expected.to eq(credentials) } + end end end describe '#full_url' do - it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") } + context 'credentials in hash' do + where(:credentials, :userinfo) do + { user: 'foo', password: 'bar' } | 'foo:bar@' + { user: 'foo', password: '' } | 'foo@' + { user: 'foo', password: nil } | 'foo@' + { user: '', password: 'bar' } | ':bar@' + { user: '', password: '' } | nil + { user: '', password: nil } | nil + { user: nil, password: 'bar' } | ':bar@' + { user: nil, password: '' } | nil + { user: nil, password: nil } | nil + end - it 'supports scp-like URLs' do - sanitizer = described_class.new('user@server:project.git') + with_them do + subject { described_class.new('http://example.com', credentials: credentials).full_url } - expect(sanitizer.full_url).to eq('user@server:project.git') + it { is_expected.to eq("http://#{userinfo}example.com") } + end end - context 'when user is given to #initialize' do - let(:url_sanitizer) do - described_class.new("https://github.com/me/project.git", credentials: { user: user.username }) + context 'credentials in URL' do + where(:input, :output) do + nil | '' + '' | :same + 'git@example.com' | :same + 'http://example.com' | :same + 'http://foo@example.com' | :same + 'http://foo:@example.com' | 'http://foo@example.com' + 'http://:bar@example.com' | :same + 'http://foo:bar@example.com' | :same end - it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") } + with_them do + let(:expected) { output == :same ? input : output } + + it { expect(described_class.new(input).full_url).to eq(expected) } + end end end end diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb new file mode 100644 index 00000000000..7125bfcab59 --- /dev/null +++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe SystemCheck::App::GitUserDefaultSSHConfigCheck do + let(:username) { '_this_user_will_not_exist_unless_it_is_stubbed' } + let(:base_dir) { Dir.mktmpdir } + let(:home_dir) { File.join(base_dir, "/var/lib/#{username}") } + let(:ssh_dir) { File.join(home_dir, '.ssh') } + let(:forbidden_file) { 'id_rsa' } + + before do + allow(Gitlab.config.gitlab).to receive(:user).and_return(username) + end + + after do + FileUtils.rm_rf(base_dir) + end + + it 'only whitelists safe files' do + expect(described_class::WHITELIST).to contain_exactly('authorized_keys', 'authorized_keys2', 'known_hosts') + end + + describe '#skip?' do + subject { described_class.new.skip? } + + where(user_exists: [true, false], home_dir_exists: [true, false]) + + with_them do + let(:expected_result) { !user_exists || !home_dir_exists } + + before do + stub_user if user_exists + stub_home_dir if home_dir_exists + end + + it { is_expected.to eq(expected_result) } + end + end + + describe '#check?' do + subject { described_class.new.check? } + + before do + stub_user + end + + it 'fails if a forbidden file exists' do + stub_ssh_file(forbidden_file) + + is_expected.to be_falsy + end + + it "succeeds if the SSH directory doesn't exist" do + FileUtils.rm_rf(ssh_dir) + + is_expected.to be_truthy + end + + it 'succeeds if all the whitelisted files exist' do + described_class::WHITELIST.each do |filename| + stub_ssh_file(filename) + end + + is_expected.to be_truthy + end + end + + def stub_user + allow(File).to receive(:expand_path).with("~#{username}").and_return(home_dir) + end + + def stub_home_dir + FileUtils.mkdir_p(home_dir) + end + + def stub_ssh_file(filename) + FileUtils.mkdir_p(ssh_dir) + FileUtils.touch(File.join(ssh_dir, filename)) + end +end diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb index 4de5da984ba..9da3648400e 100644 --- a/spec/lib/system_check/simple_executor_spec.rb +++ b/spec/lib/system_check/simple_executor_spec.rb @@ -35,6 +35,20 @@ describe SystemCheck::SimpleExecutor do end end + class DynamicSkipCheck < SystemCheck::BaseCheck + set_name 'dynamic skip check' + set_skip_reason 'this is a skip reason' + + def skip? + self.skip_reason = 'this is a dynamic skip reason' + true + end + + def check? + raise 'should not execute this' + end + end + class MultiCheck < SystemCheck::BaseCheck set_name 'multi check' @@ -127,6 +141,10 @@ describe SystemCheck::SimpleExecutor do expect(subject.checks.size).to eq(1) end + + it 'errors out when passing multiple items' do + expect { subject << [SimpleCheck, OtherCheck] }.to raise_error(ArgumentError) + end end subject { described_class.new('Test') } @@ -205,10 +223,14 @@ describe SystemCheck::SimpleExecutor do subject.run_check(SkipCheck) end - it 'displays #skip_reason' do + it 'displays .skip_reason' do expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout end + it 'displays #skip_reason' do + expect { subject.run_check(DynamicSkipCheck) }.to output(/this is a dynamic skip reason/).to_stdout + end + it 'does not execute #check when #skip? is true' do expect_any_instance_of(SkipCheck).not_to receive(:check?) diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb new file mode 100644 index 00000000000..cfd4021fbac --- /dev/null +++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20170825104051_migrate_issues_to_ghost_user.rb') + +describe MigrateIssuesToGhostUser, :migration do + describe '#up' do + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:users) { table(:users) } + + before do + projects.create!(name: 'gitlab') + user = users.create(email: 'test@example.com') + issues.create(title: 'Issue 1', author_id: nil, project_id: 1) + issues.create(title: 'Issue 2', author_id: user.id, project_id: 1) + end + + context 'when ghost user exists' do + let!(:ghost) { users.create(ghost: true, email: 'ghost@example.com') } + + it 'does not create a new user' do + expect { schema_migrate_up! }.not_to change { User.count } + end + + it 'migrates issues where author = nil to the ghost user' do + schema_migrate_up! + + expect(issues.first.reload.author_id).to eq(ghost.id) + end + + it 'does not change issues authored by an existing user' do + expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id} + end + end + + context 'when ghost user does not exist' do + it 'creates a new user' do + expect { schema_migrate_up! }.to change { User.count }.by(1) + end + + it 'migrates issues where author = nil to the ghost user' do + schema_migrate_up! + + expect(issues.first.reload.author_id).to eq(User.ghost.id) + end + + it 'does not change issues authored by an existing user' do + expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id} + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 0c35ad3c9d8..c2c9f1c12d1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -43,6 +43,32 @@ describe Ci::Build do it { is_expected.not_to include(manual_but_created) } end + describe '.ref_protected' do + subject { described_class.ref_protected } + + context 'when protected is true' do + let!(:job) { create(:ci_build, :protected) } + + it { is_expected.to include(job) } + end + + context 'when protected is false' do + let!(:job) { create(:ci_build) } + + it { is_expected.not_to include(job) } + end + + context 'when protected is nil' do + let!(:job) { create(:ci_build) } + + before do + job.update_attribute(:protected, nil) + end + + it { is_expected.not_to include(job) } + end + end + describe '#actionize' do context 'when build is a created' do before do @@ -1466,10 +1492,12 @@ describe Ci::Build do context 'when build is for triggers' do let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } + let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) } + let(:user_trigger_variable) do - { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } + { key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1', public: false } end + let(:predefined_trigger_variable) do { key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true } end @@ -1478,8 +1506,26 @@ describe Ci::Build do build.trigger_request = trigger_request end - it { is_expected.to include(user_trigger_variable) } - it { is_expected.to include(predefined_trigger_variable) } + shared_examples 'returns variables for triggers' do + it { is_expected.to include(user_trigger_variable) } + it { is_expected.to include(predefined_trigger_variable) } + end + + context 'when variables are stored in trigger_request' do + before do + trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } ) + end + + it_behaves_like 'returns variables for triggers' + end + + context 'when variables are stored in pipeline_variables' do + before do + create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1') + end + + it_behaves_like 'returns variables for triggers' + end end context 'when pipeline has a variable' do @@ -1642,6 +1688,30 @@ describe Ci::Build do { key: 'secret', value: 'value', public: false }]) end end + + context 'when using auto devops' do + context 'and is enabled' do + before do + project.create_auto_devops!(enabled: true, domain: 'example.com') + end + + it "includes AUTO_DEVOPS_DOMAIN" do + is_expected.to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + + context 'and is disabled' do + before do + project.create_auto_devops!(enabled: false, domain: 'example.com') + end + + it "includes AUTO_DEVOPS_DOMAIN" do + is_expected.not_to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + end end describe 'state transition: any => [:pending]' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 80cf872a5fd..95da97b7bc5 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -546,6 +546,22 @@ describe Ci::Pipeline, :mailer do end end + describe '#has_kubernetes_active?' do + context 'when kubernetes is active' do + let(:project) { create(:kubernetes_project) } + + it 'returns true' do + expect(pipeline).to have_kubernetes_active + end + end + + context 'when kubernetes is not active' do + it 'returns false' do + expect(pipeline).not_to have_kubernetes_active + end + end + end + describe '#has_stage_seeds?' do context 'when pipeline has stage seeds' do subject { build(:ci_pipeline_with_one_job) } @@ -783,10 +799,77 @@ describe Ci::Pipeline, :mailer do end end + describe '#set_config_source' do + context 'on object initialisation' do + context 'when pipelines does not contain needed data' do + let(:pipeline) do + Ci::Pipeline.new + end + + it 'defines source to be unknown' do + expect(pipeline).to be_unknown_source + end + end + + context 'when pipeline contains all needed data' do + let(:pipeline) do + Ci::Pipeline.new( + project: project, + sha: '1234', + ref: 'master', + source: :push) + end + + context 'when the repository has a config file' do + before do + allow(project.repository).to receive(:gitlab_ci_yml_for) + .and_return('config') + end + + it 'defines source to be from repository' do + expect(pipeline).to be_repository_source + end + + context 'when loading an object' do + let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) } + + it 'does not redefine the source' do + # force to overwrite the source + pipeline.unknown_source! + + expect(new_pipeline).to be_unknown_source + end + end + end + + context 'when the repository does not have a config file' do + let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content } + + context 'auto devops enabled' do + before do + stub_application_setting(auto_devops_enabled: true) + allow(project).to receive(:ci_config_path) { 'custom' } + end + + it 'defines source to be auto devops' do + subject + + expect(pipeline).to be_auto_devops_source + end + end + end + end + end + end + describe '#ci_yaml_file' do let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content } - context 'when AutoDevops is enabled' do + context 'the source is unknown' do + before do + pipeline.unknown_source! + end + it 'returns the configuration if found' do allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) .and_return('config') @@ -794,49 +877,39 @@ describe Ci::Pipeline, :mailer do expect(pipeline.ci_yaml_file).to be_a(String) expect(pipeline.ci_yaml_file).not_to eq(implied_yml) expect(pipeline.yaml_errors).to be_nil - expect(pipeline.repository?).to be(true) end - context 'when the implied configuration will be used' do - before do - allow_any_instance_of(ApplicationSetting) - .to receive(:auto_devops_enabled?) { true } - end - - it 'returns the implied configuration when its not found' do - allow(pipeline.project).to receive(:ci_config_path) { 'custom' } + it 'sets yaml errors if not found' do + expect(pipeline.ci_yaml_file).to be_nil + expect(pipeline.yaml_errors) + .to start_with('Failed to load CI/CD config file') + end + end - expect(pipeline.ci_yaml_file).to eq(implied_yml) - end + context 'the source is the repository' do + before do + pipeline.repository_source! + end - it 'sets the config source' do - allow(pipeline.project).to receive(:ci_config_path) { 'custom' } + it 'returns the configuration if found' do + allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) + .and_return('config') - expect(pipeline.ci_yaml_file).to eq(implied_yml) - expect(pipeline.auto_devops?).to be(true) - end + expect(pipeline.ci_yaml_file).to be_a(String) + expect(pipeline.ci_yaml_file).not_to eq(implied_yml) + expect(pipeline.yaml_errors).to be_nil end end - context 'when AudoDevOps is disabled' do - context 'when an invalid path is given' do - it 'sets the yaml errors' do - allow(pipeline.project).to receive(:ci_config_path) { 'custom' } - - expect(pipeline.ci_yaml_file).to be_nil - expect(pipeline.yaml_errors) - .to start_with('Failed to load CI/CD config file') - end + context 'when the source is auto_devops_source' do + before do + stub_application_setting(auto_devops_enabled: true) + pipeline.auto_devops_source! end - context 'when the config file can be found' do - it 'has no yaml_errors' do - allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for) - .and_return('config') - - expect(pipeline.ci_yaml_file).to eq('config') - expect(pipeline.yaml_errors).to be_nil - end + it 'finds the implied config' do + expect(pipeline.ci_yaml_file).to eq(implied_yml) + expect(pipeline.yaml_errors).to be_nil end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 48f878bbee6..2e686e515c5 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' describe Ci::Runner do describe 'validation' do + it { is_expected.to validate_presence_of(:access_level) } + context 'when runner is not allowed to pick untagged jobs' do context 'when runner does not have tags' do it 'is not valid' do @@ -19,6 +21,34 @@ describe Ci::Runner do end end + describe '#access_level' do + context 'when creating new runner and access_level is nil' do + let(:runner) do + build(:ci_runner, access_level: nil) + end + + it "object is invalid" do + expect(runner).not_to be_valid + end + end + + context 'when creating new runner and access_level is defined in enum' do + let(:runner) do + build(:ci_runner, access_level: :not_protected) + end + + it "object is valid" do + expect(runner).to be_valid + end + end + + context 'when creating new runner and access_level is not defined in enum' do + it "raises an error" do + expect { build(:ci_runner, access_level: :this_is_not_defined) }.to raise_error(ArgumentError) + end + end + end + describe '#display_name' do it 'returns the description if it has a value' do runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') @@ -95,6 +125,8 @@ describe Ci::Runner do let(:build) { create(:ci_build, pipeline: pipeline) } let(:runner) { create(:ci_runner) } + subject { runner.can_pick?(build) } + before do build.project.runners << runner end @@ -222,6 +254,50 @@ describe Ci::Runner do end end end + + context 'when access_level of runner is not_protected' do + before do + runner.not_protected! + end + + context 'when build is protected' do + before do + build.protected = true + end + + it { is_expected.to be_truthy } + end + + context 'when build is unprotected' do + before do + build.protected = false + end + + it { is_expected.to be_truthy } + end + end + + context 'when access_level of runner is ref_protected' do + before do + runner.ref_protected! + end + + context 'when build is protected' do + before do + build.protected = true + end + + it { is_expected.to be_truthy } + end + + context 'when build is unprotected' do + before do + build.protected = false + end + + it { is_expected.to be_falsey } + end + end end describe '#status' do diff --git a/spec/models/ci/trigger_request_spec.rb b/spec/models/ci/trigger_request_spec.rb new file mode 100644 index 00000000000..7dcf3528f73 --- /dev/null +++ b/spec/models/ci/trigger_request_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Ci::TriggerRequest do + describe 'validation' do + it 'be invalid if saving a variable' do + trigger = build(:ci_trigger_request, variables: { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } ) + + expect(trigger).not_to be_valid + end + + it 'be valid if not saving a variable' do + trigger = build(:ci_trigger_request) + + expect(trigger).to be_valid + end + end +end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index f7583645e69..858ec831200 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -443,4 +443,25 @@ describe CommitStatus do end end end + + describe 'set failure_reason when drop' do + let(:commit_status) { create(:commit_status, :created) } + + subject do + commit_status.drop!(reason) + commit_status + end + + context 'when failure_reason is nil' do + let(:reason) { } + + it { is_expected.to be_unknown_failure } + end + + context 'when failure_reason is script_failure' do + let(:reason) { :script_failure } + + it { is_expected.to be_script_failure } + end + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index dfbe1a7c192..fb5fb7daaab 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -66,56 +66,76 @@ describe Issuable do end describe ".search" do - let!(:searchable_issue) { create(:issue, title: "Searchable issue") } + let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") } - it 'returns notes with a matching title' do + it 'returns issues with a matching title' do expect(issuable_class.search(searchable_issue.title)) .to eq([searchable_issue]) end - it 'returns notes with a partially matching title' do + it 'returns issues with a partially matching title' do expect(issuable_class.search('able')).to eq([searchable_issue]) end - it 'returns notes with a matching title regardless of the casing' do + it 'returns issues with a matching title regardless of the casing' do expect(issuable_class.search(searchable_issue.title.upcase)) .to eq([searchable_issue]) end + + it 'returns issues with a fuzzy matching title' do + expect(issuable_class.search('searchable issue')).to eq([searchable_issue]) + end + + it 'returns all issues with a query shorter than 3 chars' do + expect(issuable_class.search('zz')).to eq(issuable_class.all) + end end describe ".full_search" do let!(:searchable_issue) do - create(:issue, title: "Searchable issue", description: 'kittens') + create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens') end - it 'returns notes with a matching title' do + it 'returns issues with a matching title' do expect(issuable_class.full_search(searchable_issue.title)) .to eq([searchable_issue]) end - it 'returns notes with a partially matching title' do + it 'returns issues with a partially matching title' do expect(issuable_class.full_search('able')).to eq([searchable_issue]) end - it 'returns notes with a matching title regardless of the casing' do + it 'returns issues with a matching title regardless of the casing' do expect(issuable_class.full_search(searchable_issue.title.upcase)) .to eq([searchable_issue]) end - it 'returns notes with a matching description' do + it 'returns issues with a fuzzy matching title' do + expect(issuable_class.full_search('searchable issue')).to eq([searchable_issue]) + end + + it 'returns issues with a matching description' do expect(issuable_class.full_search(searchable_issue.description)) .to eq([searchable_issue]) end - it 'returns notes with a partially matching description' do + it 'returns issues with a partially matching description' do expect(issuable_class.full_search(searchable_issue.description)) .to eq([searchable_issue]) end - it 'returns notes with a matching description regardless of the casing' do + it 'returns issues with a matching description regardless of the casing' do expect(issuable_class.full_search(searchable_issue.description.upcase)) .to eq([searchable_issue]) end + + it 'returns issues with a fuzzy matching description' do + expect(issuable_class.full_search('many kittens')).to eq([searchable_issue]) + end + + it 'returns all issues with a query shorter than 3 chars' do + expect(issuable_class.search('zz')).to eq(issuable_class.all) + end end describe '.to_ability_name' do @@ -460,4 +480,71 @@ describe Issuable do end end end + + describe '#first_contribution?' do + let(:group) { create(:group) } + let(:project) { create(:project, namespace: group) } + let(:other_project) { create(:project) } + let(:owner) { create(:owner) } + let(:master) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } + + let(:contributor) { create(:user) } + let(:first_time_contributor) { create(:user) } + + before do + group.add_owner(owner) + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) + project.add_guest(contributor) + project.add_guest(first_time_contributor) + end + + let(:merged_mr) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) } + let(:open_mr) { create(:merge_request, author: first_time_contributor, target_project: project, source_project: project) } + let(:merged_mr_other_project) { create(:merge_request, :merged, author: first_time_contributor, target_project: other_project, source_project: other_project) } + + context "for merge requests" do + it "is false for MASTER" do + mr = create(:merge_request, author: master, target_project: project, source_project: project) + + expect(mr).not_to be_first_contribution + end + + it "is false for OWNER" do + mr = create(:merge_request, author: owner, target_project: project, source_project: project) + + expect(mr).not_to be_first_contribution + end + + it "is false for REPORTER" do + mr = create(:merge_request, author: reporter, target_project: project, source_project: project) + + expect(mr).not_to be_first_contribution + end + + it "is true when you don't have any merged MR" do + expect(open_mr).to be_first_contribution + expect(merged_mr).not_to be_first_contribution + end + + it "handles multiple projects separately" do + expect(open_mr).to be_first_contribution + expect(merged_mr_other_project).not_to be_first_contribution + end + end + + context "for issues" do + let(:contributor_issue) { create(:issue, author: contributor, project: project) } + let(:first_time_contributor_issue) { create(:issue, author: first_time_contributor, project: project) } + + it "is false even without merged MR" do + expect(merged_mr).to be + expect(first_time_contributor_issue).not_to be_first_contribution + expect(contributor_issue).not_to be_first_contribution + end + end + end end diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb index d00faa4f8be..91591017587 100644 --- a/spec/models/concerns/resolvable_note_spec.rb +++ b/spec/models/concerns/resolvable_note_spec.rb @@ -189,8 +189,8 @@ describe Note, ResolvableNote do allow(subject).to receive(:resolvable?).and_return(false) end - it "returns nil" do - expect(subject.resolve!(current_user)).to be_nil + it "returns false" do + expect(subject.resolve!(current_user)).to be_falsey end it "doesn't set resolved_at" do @@ -224,8 +224,8 @@ describe Note, ResolvableNote do subject.resolve!(user) end - it "returns nil" do - expect(subject.resolve!(current_user)).to be_nil + it "returns false" do + expect(subject.resolve!(current_user)).to be_falsey end it "doesn't change resolved_at" do @@ -279,8 +279,8 @@ describe Note, ResolvableNote do allow(subject).to receive(:resolvable?).and_return(false) end - it "returns nil" do - expect(subject.unresolve!).to be_nil + it "returns false" do + expect(subject.unresolve!).to be_falsey end end @@ -320,8 +320,8 @@ describe Note, ResolvableNote do end context "when not resolved" do - it "returns nil" do - expect(subject.unresolve!).to be_nil + it "returns false" do + expect(subject.unresolve!).to be_falsey end end end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index e48f20bf53b..9c99c3e5c08 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -99,14 +99,14 @@ describe GpgKey do end describe '#verified?' do - it 'returns true one of the email addresses in the key belongs to the user' do + it 'returns true if one of the email addresses in the key belongs to the user' do user = create :user, email: 'bette.cartwright@example.com' gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user expect(gpg_key.verified?).to be_truthy end - it 'returns false if one of the email addresses in the key does not belong to the user' do + it 'returns false if none of the email addresses in the key does not belong to the user' do user = create :user, email: 'someone.else@example.com' gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user @@ -114,6 +114,32 @@ describe GpgKey do end end + describe 'verified_and_belongs_to_email?' do + it 'returns false if none of the email addresses in the key does not belong to the user' do + user = create :user, email: 'someone.else@example.com' + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user + + expect(gpg_key.verified?).to be_falsey + expect(gpg_key.verified_and_belongs_to_email?('someone.else@example.com')).to be_falsey + end + + it 'returns false if one of the email addresses in the key belongs to the user and does not match the provided email' do + user = create :user, email: 'bette.cartwright@example.com' + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user + + expect(gpg_key.verified?).to be_truthy + expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.net')).to be_falsey + end + + it 'returns true if one of the email addresses in the key belongs to the user and matches the provided email' do + user = create :user, email: 'bette.cartwright@example.com' + gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user + + expect(gpg_key.verified?).to be_truthy + expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.com')).to be_truthy + end + end + describe 'notification', :mailer do let(:user) { create(:user) } @@ -129,15 +155,15 @@ describe GpgKey do describe '#revoke' do it 'invalidates all associated gpg signatures and destroys the key' do gpg_key = create :gpg_key - gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: gpg_key + gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: gpg_key unrelated_gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key - unrelated_gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: unrelated_gpg_key + unrelated_gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: unrelated_gpg_key gpg_key.revoke expect(gpg_signature.reload).to have_attributes( - valid_signature: false, + verification_status: 'unknown_key', gpg_key: nil ) @@ -145,7 +171,7 @@ describe GpgKey do # unrelated signature is left untouched expect(unrelated_gpg_signature.reload).to have_attributes( - valid_signature: true, + verification_status: 'verified', gpg_key: unrelated_gpg_key ) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index f9cd12c0ff3..f36d6eeb327 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -9,6 +9,7 @@ describe Group do it { is_expected.to have_many(:users).through(:group_members) } it { is_expected.to have_many(:owners).through(:group_members) } it { is_expected.to have_many(:requesters).dependent(:destroy) } + it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:project_group_links).dependent(:destroy) } it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } @@ -25,22 +26,8 @@ describe Group do group.add_developer(developer) end - describe '#members' do - it 'includes members and exclude requesters' do - member_user_ids = group.members.pluck(:user_id) - - expect(member_user_ids).to include(developer.id) - expect(member_user_ids).not_to include(requester.id) - end - end - - describe '#requesters' do - it 'does not include requesters' do - requester_user_ids = group.requesters.pluck(:user_id) - - expect(requester_user_ids).to include(requester.id) - expect(requester_user_ids).not_to include(developer.id) - end + it_behaves_like 'members and requesters associations' do + let(:namespace) { group } end end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 87513e18b25..a07ce05a865 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -409,6 +409,15 @@ describe Member do expect(members).to be_a Array expect(members).to be_empty end + + it 'supports differents formats' do + list = ['joe@local.test', admin, user1.id, user2.id.to_s] + + members = described_class.add_users(source, list, :master) + + expect(members.size).to eq(4) + expect(members.first).to be_invite + end end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f5d079c27c4..d80d5657c42 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1262,7 +1262,6 @@ describe MergeRequest do describe "#reload_diff" do let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion } - let(:commit) { subject.project.commit(sample_commit.id) } it "does not change existing merge request diff" do @@ -1280,9 +1279,19 @@ describe MergeRequest do subject.reload_diff end - it "updates diff discussion positions" do - old_diff_refs = subject.diff_refs + it "calls update_diff_discussion_positions" do + expect(subject).to receive(:update_diff_discussion_positions) + + subject.reload_diff + end + end + describe '#update_diff_discussion_positions' do + let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion } + let(:commit) { subject.project.commit(sample_commit.id) } + let(:old_diff_refs) { subject.diff_refs } + + before do # Update merge_request_diff so that #diff_refs will return commit.diff_refs allow(subject).to receive(:create_merge_request_diff) do subject.merge_request_diffs.create( @@ -1293,7 +1302,9 @@ describe MergeRequest do subject.merge_request_diff(true) end + end + it "updates diff discussion positions" do expect(Discussions::UpdateDiffPositionService).to receive(:new).with( subject.project, subject.author, @@ -1305,7 +1316,26 @@ describe MergeRequest do expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original expect_any_instance_of(DiffNote).to receive(:save).once - subject.reload_diff(subject.author) + subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs, + new_diff_refs: commit.diff_refs, + current_user: subject.author) + end + + context 'when resolve_outdated_diff_discussions is set' do + before do + discussion + + subject.project.update!(resolve_outdated_diff_discussions: true) + end + + it 'calls MergeRequests::ResolvedDiscussionNotificationService' do + expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService) + .to receive(:execute).with(subject) + + subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs, + new_diff_refs: commit.diff_refs, + current_user: subject.author) + end end end diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index 08cb7c4c1b1..ca13af4d73e 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -1,14 +1,23 @@ require 'spec_helper' describe ProjectAutoDevops do - subject { build_stubbed(:project_auto_devops) } + set(:project) { build(:project) } it { is_expected.to belong_to(:project) } - it { is_expected.to validate_presence_of(:domain) } - it { is_expected.to respond_to(:created_at) } it { is_expected.to respond_to(:updated_at) } - it { is_expected.to be_enabled } + describe 'variables' do + let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) } + + context 'when domain is defined' do + let(:domain) { 'example.com' } + + it 'returns AUTO_DEVOPS_DOMAIN' do + expect(auto_devops.variables).to include( + { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }) + end + end + end end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index b1743cd608e..537cdadd528 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -203,18 +203,13 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do describe '#predefined_variables' do let(:kubeconfig) do - config = - YAML.load(File.read(expand_fixture_path('config/kubeconfig.yml'))) - - config.dig('users', 0, 'user')['token'] = - 'token' - + config_file = expand_fixture_path('config/kubeconfig.yml') + config = YAML.load(File.read(config_file)) + config.dig('users', 0, 'user')['token'] = 'token' + config.dig('contexts', 0, 'context')['namespace'] = namespace config.dig('clusters', 0, 'cluster')['certificate-authority-data'] = Base64.encode64('CA PEM DATA') - config.dig('contexts', 0, 'context')['namespace'] = - namespace - YAML.dump(config) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d41d7150922..75c99b62150 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -75,6 +75,7 @@ describe Project do it { is_expected.to have_many(:forks).through(:forked_project_links) } it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:pipeline_schedules) } + it { is_expected.to have_many(:members_and_requesters) } context 'after initialized' do it "has a project_feature" do @@ -91,22 +92,8 @@ describe Project do project.team << [developer, :developer] end - describe '#members' do - it 'includes members and exclude requesters' do - member_user_ids = project.members.pluck(:user_id) - - expect(member_user_ids).to include(developer.id) - expect(member_user_ids).not_to include(requester.id) - end - end - - describe '#requesters' do - it 'does not include requesters' do - requester_user_ids = project.requesters.pluck(:user_id) - - expect(requester_user_ids).to include(requester.id) - expect(requester_user_ids).not_to include(developer.id) - end + it_behaves_like 'members and requesters associations' do + let(:namespace) { project } end end @@ -2522,4 +2509,133 @@ describe Project do end end end + + describe '#has_ci?' do + set(:project) { create(:project) } + let(:repository) { double } + + before do + expect(project).to receive(:repository) { repository } + end + + context 'when has .gitlab-ci.yml' do + before do + expect(repository).to receive(:gitlab_ci_yml) { 'content' } + end + + it "CI is available" do + expect(project).to have_ci + end + end + + context 'when there is no .gitlab-ci.yml' do + before do + expect(repository).to receive(:gitlab_ci_yml) { nil } + end + + it "CI is not available" do + expect(project).not_to have_ci + end + + context 'when auto devops is enabled' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it "CI is available" do + expect(project).to have_ci + end + end + end + end + + describe '#auto_devops_enabled?' do + set(:project) { create(:project) } + + subject { project.auto_devops_enabled? } + + context 'when enabled in settings' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it 'auto devops is implicitly enabled' do + expect(project.auto_devops).to be_nil + expect(project).to be_auto_devops_enabled + end + + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project) + end + + it "auto devops is enabled" do + expect(project).to be_auto_devops_enabled + end + end + + context 'when explicitly disabled' do + before do + create(:project_auto_devops, project: project, enabled: false) + end + + it "auto devops is disabled" do + expect(project).not_to be_auto_devops_enabled + end + end + end + + context 'when disabled in settings' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it 'auto devops is implicitly disabled' do + expect(project.auto_devops).to be_nil + expect(project).not_to be_auto_devops_enabled + end + + context 'when explicitly enabled' do + before do + create(:project_auto_devops, project: project) + end + + it "auto devops is enabled" do + expect(project).to be_auto_devops_enabled + end + end + end + end + + context '#auto_devops_variables' do + set(:project) { create(:project) } + + subject { project.auto_devops_variables } + + context 'when enabled in settings' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + context 'when domain is empty' do + before do + create(:project_auto_devops, project: project, domain: nil) + end + + it 'variables are empty' do + is_expected.to be_empty + end + end + + context 'when domain is configured' do + before do + create(:project_auto_devops, project: project, domain: 'example.com') + end + + it "variables are not empty" do + is_expected.not_to be_empty + end + end + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 40875c8fb7e..7065d467ec0 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -886,7 +886,7 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .with(committer, repository, old_rev, blank_sha, 'refs/heads/feature') + .with(committer, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -932,20 +932,20 @@ describe Repository, models: true do service = Gitlab::Git::HooksService.new expect(Gitlab::Git::HooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with(committer, target_repository, old_rev, new_rev, updating_ref) + .with(committer, target_repository.raw_repository, old_rev, new_rev, updating_ref) .and_yield(service).and_return(true) end it 'runs without errors' do expect do - GitOperationService.new(committer, repository).with_branch('feature') do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do new_rev end end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do - service = GitOperationService.new(committer, repository) + service = Gitlab::Git::OperationService.new(committer, repository.raw_repository) expect(service).to receive(:update_autocrlf_option) @@ -956,7 +956,7 @@ describe Repository, models: true do it 'updates the head' do expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - GitOperationService.new(committer, repository).with_branch('feature') do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do new_rev end @@ -971,13 +971,13 @@ describe Repository, models: true do let(:updating_ref) { 'refs/heads/master' } it 'fetch_ref and create the branch' do - expect(target_project.repository).to receive(:fetch_ref) + expect(target_project.repository.raw_repository).to receive(:fetch_ref) .and_call_original - GitOperationService.new(committer, target_repository) + Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) .with_branch( 'master', - start_project: project, + start_repository: project.repository.raw_repository, start_branch_name: 'feature') { new_rev } expect(target_repository.branch_names).to contain_exactly('master') @@ -990,8 +990,8 @@ describe Repository, models: true do it 'does not fetch_ref and just pass the commit' do expect(target_repository).not_to receive(:fetch_ref) - GitOperationService.new(committer, target_repository) - .with_branch('feature', start_project: project) { new_rev } + Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) + .with_branch('feature', start_repository: project.repository.raw_repository) { new_rev } end end end @@ -1000,7 +1000,7 @@ describe Repository, models: true do let(:target_project) { create(:project, :empty_repo) } before do - expect(target_project.repository).to receive(:run_git) + expect(target_project.repository.raw_repository).to receive(:run_git) end it 'raises Rugged::ReferenceError' do @@ -1009,9 +1009,9 @@ describe Repository, models: true do end expect do - GitOperationService.new(committer, target_project.repository) + Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository) .with_branch('feature', - start_project: project, + start_repository: project.repository.raw_repository, &:itself) end.to raise_reference_error end @@ -1031,7 +1031,7 @@ describe Repository, models: true do repository.add_branch(user, branch, old_rev) expect do - GitOperationService.new(committer, repository).with_branch(branch) do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do new_rev end end.not_to raise_error @@ -1049,10 +1049,10 @@ describe Repository, models: true do # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. expect do - GitOperationService.new(committer, repository).with_branch(branch) do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do new_rev end - end.to raise_error(Repository::CommitError) + end.to raise_error(Gitlab::Git::CommitError) end end @@ -1061,7 +1061,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - GitOperationService.new(committer, repository).with_branch('feature') do + Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do new_rev end end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) @@ -1079,10 +1079,9 @@ describe Repository, models: true do expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_branches_cache) - GitOperationService.new(committer, repository) - .with_branch('new-feature') do - new_rev - end + repository.with_branch(user, 'new-feature') do + new_rev + end end end @@ -1139,7 +1138,7 @@ describe Repository, models: true do describe 'when there are no branches' do before do - allow(repository).to receive(:branch_count).and_return(0) + allow(repository.raw_repository).to receive(:branch_count).and_return(0) end it { is_expected.to eq(false) } @@ -1147,7 +1146,7 @@ describe Repository, models: true do describe 'when there are branches' do it 'returns true' do - expect(repository).to receive(:branch_count).and_return(3) + expect(repository.raw_repository).to receive(:branch_count).and_return(3) expect(subject).to eq(true) end @@ -1161,7 +1160,7 @@ describe Repository, models: true do end it 'sets autocrlf to :input' do - GitOperationService.new(nil, repository).send(:update_autocrlf_option) + Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option) expect(repository.raw_repository.autocrlf).to eq(:input) end @@ -1176,7 +1175,7 @@ describe Repository, models: true do expect(repository.raw_repository).not_to receive(:autocrlf=) .with(:input) - GitOperationService.new(nil, repository).send(:update_autocrlf_option) + Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option) end end end @@ -1762,15 +1761,15 @@ describe Repository, models: true do describe '#update_ref' do it 'can create a ref' do - GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository.find_branch('foobar')).not_to be_nil end it 'raises CommitError when the ref update fails' do expect do - GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) - end.to raise_error(Repository::CommitError) + Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + end.to raise_error(Gitlab::Git::CommitError) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b70ab5581ac..abf732e60bf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2102,4 +2102,84 @@ describe User do end end end + + describe '#verified_email?' do + it 'returns true when the email is the primary email' do + user = build :user, email: 'email@example.com' + + expect(user.verified_email?('email@example.com')).to be true + end + + it 'returns false when the email is not the primary email' do + user = build :user, email: 'email@example.com' + + expect(user.verified_email?('other_email@example.com')).to be false + end + end + + describe '#sync_attribute?' do + let(:user) { described_class.new } + + context 'oauth user' do + it 'returns true if name can be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(name location)) + expect(user.sync_attribute?(:name)).to be_truthy + end + + it 'returns true if email can be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(name email)) + expect(user.sync_attribute?(:email)).to be_truthy + end + + it 'returns true if location can be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(location email)) + expect(user.sync_attribute?(:email)).to be_truthy + end + + it 'returns false if name can not be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(location email)) + expect(user.sync_attribute?(:name)).to be_falsey + end + + it 'returns false if email can not be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(location email)) + expect(user.sync_attribute?(:name)).to be_falsey + end + + it 'returns false if location can not be synced' do + stub_omniauth_setting(sync_profile_attributes: %w(location email)) + expect(user.sync_attribute?(:name)).to be_falsey + end + + it 'returns true for all syncable attributes if all syncable attributes can be synced' do + stub_omniauth_setting(sync_profile_attributes: true) + expect(user.sync_attribute?(:name)).to be_truthy + expect(user.sync_attribute?(:email)).to be_truthy + expect(user.sync_attribute?(:location)).to be_truthy + end + + it 'returns false for all syncable attributes but email if no syncable attributes are declared' do + expect(user.sync_attribute?(:name)).to be_falsey + expect(user.sync_attribute?(:email)).to be_truthy + expect(user.sync_attribute?(:location)).to be_falsey + end + end + + context 'ldap user' do + it 'returns true for email if ldap user' do + allow(user).to receive(:ldap_user?).and_return(true) + expect(user.sync_attribute?(:name)).to be_falsey + expect(user.sync_attribute?(:email)).to be_truthy + expect(user.sync_attribute?(:location)).to be_falsey + end + + it 'returns true for email and location if ldap user and location declared as syncable' do + allow(user).to receive(:ldap_user?).and_return(true) + stub_omniauth_setting(sync_profile_attributes: %w(location)) + expect(user.sync_attribute?(:name)).to be_falsey + expect(user.sync_attribute?(:email)).to be_truthy + expect(user.sync_attribute?(:location)).to be_truthy + end + end + end end diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index a7a34ecac72..1a8001be6ab 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -100,4 +100,38 @@ describe Ci::BuildPresenter do end end end + + describe '#trigger_variables' do + let(:build) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) } + let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) } + + context 'when variable is stored in ci_pipeline_variables' do + let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline) } + + context 'when pipeline is triggered by trigger API' do + it 'returns variables' do + expect(presenter.trigger_variables).to eq([pipeline_variable.to_runner_variable]) + end + end + + context 'when pipeline is not triggered by trigger API' do + let(:build) { create(:ci_build, pipeline: pipeline) } + + it 'does not return variables' do + expect(presenter.trigger_variables).to eq([]) + end + end + end + + context 'when variable is stored in ci_trigger_requests.variables' do + before do + trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } ) + end + + it 'returns variables' do + expect(presenter.trigger_variables).to eq(trigger_request.user_variables) + end + end + end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index b1e011de604..cc794fad3a7 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -75,6 +75,22 @@ describe API::Branches do let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}" } shared_examples_for 'repository branch' do + context 'HEAD request' do + it 'returns 204 No Content' do + head api(route, user) + + expect(response).to have_gitlab_http_status(204) + expect(response.body).to be_empty + end + + it 'returns 404 Not Found' do + head api("/projects/#{project_id}/repository/branches/unknown", user) + + expect(response).to have_gitlab_http_status(404) + expect(response.body).to be_empty + end + end + it 'returns the repository branch' do get api(route, current_user) diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 3c02e6302b4..e4c73583545 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -16,8 +16,8 @@ describe API::CommitStatuses do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } context 'ci commit exists' do - let!(:master) { project.pipelines.create(source: :push, sha: commit.id, ref: 'master') } - let!(:develop) { project.pipelines.create(source: :push, sha: commit.id, ref: 'develop') } + let!(:master) { project.pipelines.create(source: :push, sha: commit.id, ref: 'master', protected: false) } + let!(:develop) { project.pipelines.create(source: :push, sha: commit.id, ref: 'develop', protected: false) } context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } @@ -142,6 +142,9 @@ describe API::CommitStatuses do expect(json_response['ref']).not_to be_empty expect(json_response['target_url']).to be_nil expect(json_response['description']).to be_nil + if status == 'failed' + expect(CommitStatus.find(json_response['id'])).to be_api_failure + end end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index d3b48f948f6..f663719d28c 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -565,7 +565,7 @@ describe API::Commits do end context 'when the ref has a pipeline' do - let!(:pipeline) { project.pipelines.create(source: :push, ref: 'master', sha: commit.sha) } + let!(:pipeline) { project.pipelines.create(source: :push, ref: 'master', sha: commit.sha, protected: false) } it 'includes a "created" status' do get api(route, current_user) @@ -673,6 +673,12 @@ describe API::Commits do it_behaves_like 'ref diff' end end + + context 'when binary diff are treated as text' do + let(:commit_id) { TestEnv::BRANCH_SHA['add-pdf-text-binary'] } + + it_behaves_like 'ref diff' + end end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 971eaf837cb..114019441a3 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -224,7 +224,7 @@ describe API::Files do it "returns a 400 if editor fails to create file" do allow_any_instance_of(Repository).to receive(:create_file) - .and_raise(Repository::CommitError, 'Cannot create file') + .and_raise(Gitlab::Git::CommitError, 'Cannot create file') post api(route("any%2Etxt"), user), valid_params @@ -339,7 +339,7 @@ describe API::Files do end it "returns a 400 if fails to delete file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file') + allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file') delete api(route(file_path), user), valid_params diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index a6c804fb2b3..1274e66bb4c 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -5,13 +5,26 @@ describe API::Internal do let(:key) { create(:key, user: user) } let(:project) { create(:project, :repository) } let(:secret_token) { Gitlab::Shell.secret_token } + let(:gl_repository) { "project-#{project.id}" } + let(:reference_counter) { double('ReferenceCounter') } describe "GET /internal/check" do it do + expect_any_instance_of(Redis).to receive(:ping).and_return('PONG') + get api("/internal/check"), secret_token: secret_token expect(response).to have_http_status(200) expect(json_response['api_version']).to eq(API::API.version) + expect(json_response['redis']).to be(true) + end + + it 'returns false for field `redis` when redis is unavailable' do + expect_any_instance_of(Redis).to receive(:ping).and_raise(Errno::ENOENT) + + get api("/internal/check"), secret_token: secret_token + + expect(json_response['redis']).to be(false) end end @@ -661,9 +674,7 @@ describe API::Internal do # end describe 'POST /internal/post_receive' do - let(:gl_repository) { "project-#{project.id}" } let(:identifier) { 'key-123' } - let(:reference_counter) { double('ReferenceCounter') } let(:valid_params) do { @@ -749,6 +760,22 @@ describe API::Internal do end end + describe 'POST /internal/pre_receive' do + let(:valid_params) do + { gl_repository: gl_repository, secret_token: secret_token } + end + + it 'decreases the reference counter and returns the result' do + expect(Gitlab::ReferenceCounter).to receive(:new).with(gl_repository) + .and_return(reference_counter) + expect(reference_counter).to receive(:increase).and_return(true) + + post api("/internal/pre_receive"), valid_params + + expect(json_response['reference_counter_increased']).to be(true) + end + end + def project_with_repo_path(path) double().tap do |fake_project| allow(fake_project).to receive_message_chain('repository.path_to_repo' => path) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index dee75c96b86..1583d1c2435 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -138,6 +138,16 @@ describe API::Issues, :mailer do expect(first_issue['id']).to eq(issue2.id) end + it 'returns issues reacted by the authenticated user by the given emoji' do + issue2 = create(:issue, project: project, author: user, assignees: [user]) + award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star') + + get api('/issues', user2), my_reaction_emoji: award_emoji.name, scope: 'all' + + expect_paginated_array_response(size: 1) + expect(first_issue['id']).to eq(issue2.id) + end + it 'returns issues matching given search string for title' do get api("/issues", user), search: issue.title diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index f56baf9663d..2d7cc1a1798 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' describe API::Jobs do - let!(:project) do + set(:project) do create(:project, :repository, public_builds: false) end - let!(:pipeline) do + set(:pipeline) do create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) @@ -188,6 +188,84 @@ describe API::Jobs do end end + describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do + context 'when job has artifacts' do + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + + let(:artifact) do + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + end + + context 'when user is anonymous' do + let(:api_user) { nil } + + context 'when project is public' do + it 'allows to access artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, true) + + get_artifact_file(artifact) + + expect(response).to have_http_status(200) + end + end + + context 'when project is public with builds access disabled' do + it 'rejects access to artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PUBLIC) + project.update_column(:public_builds, false) + + get_artifact_file(artifact) + + expect(response).to have_http_status(403) + end + end + + context 'when project is private' do + it 'rejects access and hides existence of artifacts' do + project.update_column(:visibility_level, + Gitlab::VisibilityLevel::PRIVATE) + project.update_column(:public_builds, true) + + get_artifact_file(artifact) + + expect(response).to have_http_status(404) + end + end + end + + context 'when user is authorized' do + it 'returns a specific artifact file for a valid path' do + expect(Gitlab::Workhorse) + .to receive(:send_artifacts_entry) + .and_call_original + + get_artifact_file(artifact) + + expect(response).to have_http_status(200) + expect(response.headers) + .to include('Content-Type' => 'application/json', + 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/) + end + end + end + + context 'when job does not have artifacts' do + it 'does not return job artifact file' do + get_artifact_file('some/artifact') + + expect(response).to have_http_status(404) + end + end + + def get_artifact_file(artifact_path) + get api("/projects/#{project.id}/jobs/#{job.id}/" \ + "artifacts/#{artifact_path}", api_user) + end + end + describe 'GET /projects/:id/jobs/:job_id/artifacts' do before do get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user) @@ -209,11 +287,12 @@ describe API::Jobs do end end - context 'unauthorized user' do + context 'when anonymous user is accessing private artifacts' do let(:api_user) { nil } - it 'does not return specific job artifacts' do - expect(response).to have_http_status(401) + it 'hides artifacts and rejects request' do + expect(project).to be_private + expect(response).to have_http_status(404) end end end @@ -242,8 +321,9 @@ describe API::Jobs do get_for_ref end - it 'gives 401' do - expect(response).to have_http_status(401) + it 'does not find a resource in a private project' do + expect(project).to be_private + expect(response).to have_http_status(404) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 9027090aabd..21d2c9644fb 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -117,6 +117,18 @@ describe API::MergeRequests do expect(json_response.length).to eq(1) expect(json_response.first['id']).to eq(merge_request3.id) end + + it 'returns merge requests reacted by the authenticated user by the given emoji' do + merge_request3 = create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch') + award_emoji = create(:award_emoji, awardable: merge_request3, user: user2, name: 'star') + + get api('/merge_requests', user2), my_reaction_emoji: award_emoji.name, scope: 'all' + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(merge_request3.id) + end end end diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb index b6a5a7ffbb5..f650df57383 100644 --- a/spec/requests/api/pipeline_schedules_spec.rb +++ b/spec/requests/api/pipeline_schedules_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::PipelineSchedules do set(:developer) { create(:user) } set(:user) { create(:user) } - set(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository, public_builds: false) } before do project.add_developer(developer) @@ -110,6 +110,18 @@ describe API::PipelineSchedules do end end + context 'authenticated user with insufficient permissions' do + before do + project.add_guest(user) + end + + it 'does not return pipeline_schedules list' do + get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user) + + expect(response).to have_http_status(:not_found) + end + end + context 'unauthenticated user' do it 'does not return pipeline_schedules list' do get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") @@ -299,4 +311,150 @@ describe API::PipelineSchedules do end end end + + describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables' do + let(:params) { attributes_for(:ci_pipeline_schedule_variable) } + + set(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + context 'authenticated user with valid permissions' do + context 'with required parameters' do + it 'creates pipeline_schedule_variable' do + expect do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer), + params + end.to change { pipeline_schedule.variables.count }.by(1) + + expect(response).to have_http_status(:created) + expect(response).to match_response_schema('pipeline_schedule_variable') + expect(json_response['key']).to eq(params[:key]) + expect(json_response['value']).to eq(params[:value]) + end + end + + context 'without required parameters' do + it 'does not create pipeline_schedule_variable' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer) + + expect(response).to have_http_status(:bad_request) + end + end + + context 'when key has validation error' do + it 'does not create pipeline_schedule_variable' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer), + params.merge('key' => '!?!?') + + expect(response).to have_http_status(:bad_request) + expect(json_response['message']).to have_key('key') + end + end + end + + context 'authenticated user with invalid permissions' do + it 'does not create pipeline_schedule_variable' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", user), params + + expect(response).to have_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not create pipeline_schedule_variable' do + post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables"), params + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do + set(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + let(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule) + end + + context 'authenticated user with valid permissions' do + it 'updates pipeline_schedule_variable' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer), + value: 'updated_value' + + expect(response).to have_http_status(:ok) + expect(response).to match_response_schema('pipeline_schedule_variable') + expect(json_response['value']).to eq('updated_value') + end + end + + context 'authenticated user with invalid permissions' do + it 'does not update pipeline_schedule_variable' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", user) + + expect(response).to have_http_status(:not_found) + end + end + + context 'unauthenticated user' do + it 'does not update pipeline_schedule_variable' do + put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}") + + expect(response).to have_http_status(:unauthorized) + end + end + end + + describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do + let(:master) { create(:user) } + + set(:pipeline_schedule) do + create(:ci_pipeline_schedule, project: project, owner: developer) + end + + let!(:pipeline_schedule_variable) do + create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule) + end + + before do + project.add_master(master) + end + + context 'authenticated user with valid permissions' do + it 'deletes pipeline_schedule_variable' do + expect do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", master) + end.to change { Ci::PipelineScheduleVariable.count }.by(-1) + + expect(response).to have_http_status(:accepted) + expect(response).to match_response_schema('pipeline_schedule_variable') + end + + it 'responds with 404 Not Found if requesting non-existing pipeline_schedule_variable' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/____", master) + + expect(response).to have_http_status(:not_found) + end + end + + context 'authenticated user with invalid permissions' do + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master) } + + it 'does not delete pipeline_schedule_variable' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer) + + expect(response).to have_http_status(:forbidden) + end + end + + context 'unauthenticated user' do + it 'does not delete pipeline_schedule_variable' do + delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}") + + expect(response).to have_http_status(:unauthorized) + end + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 4490e50702b..f771e4fa4ff 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -414,6 +414,7 @@ describe API::Projects do jobs_enabled: false, merge_requests_enabled: false, wiki_enabled: false, + resolve_outdated_diff_discussions: false, only_allow_merge_if_pipeline_succeeds: false, request_access_enabled: true, only_allow_merge_if_all_discussions_are_resolved: false, @@ -477,20 +478,40 @@ describe API::Projects do expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif") end + it 'sets a project as allowing outdated diff discussions to automatically resolve' do + project = attributes_for(:project, resolve_outdated_diff_discussions: false) + + post api('/projects', user), project + + expect(json_response['resolve_outdated_diff_discussions']).to be_falsey + end + + it 'sets a project as allowing outdated diff discussions to automatically resolve if resolve_outdated_diff_discussions' do + project = attributes_for(:project, resolve_outdated_diff_discussions: true) + + post api('/projects', user), project + + expect(json_response['resolve_outdated_diff_discussions']).to be_truthy + end + it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) + project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false) + post api('/projects', user), project + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do - project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true }) + project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true) + post api('/projects', user), project + expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end it 'sets a project as allowing merge even if discussions are unresolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false) post api('/projects', user), project @@ -506,7 +527,7 @@ describe API::Projects do end it 'sets a project as allowing merge only if all discussions are resolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true) post api('/projects', user), project @@ -514,7 +535,7 @@ describe API::Projects do end it 'ignores import_url when it is nil' do - project = attributes_for(:project, { import_url: nil }) + project = attributes_for(:project, import_url: nil) post api('/projects', user), project @@ -642,20 +663,36 @@ describe API::Projects do expect(json_response['visibility']).to eq('private') end + it 'sets a project as allowing outdated diff discussions to automatically resolve' do + project = attributes_for(:project, resolve_outdated_diff_discussions: false) + + post api("/projects/user/#{user.id}", admin), project + + expect(json_response['resolve_outdated_diff_discussions']).to be_falsey + end + + it 'sets a project as allowing outdated diff discussions to automatically resolve' do + project = attributes_for(:project, resolve_outdated_diff_discussions: true) + + post api("/projects/user/#{user.id}", admin), project + + expect(json_response['resolve_outdated_diff_discussions']).to be_truthy + end + it 'sets a project as allowing merge even if build fails' do - project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) + project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false) post api("/projects/user/#{user.id}", admin), project expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey end - it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do - project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true }) + it 'sets a project as allowing merge only if pipeline succeeds' do + project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true) post api("/projects/user/#{user.id}", admin), project expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy end it 'sets a project as allowing merge even if discussions are unresolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false) post api("/projects/user/#{user.id}", admin), project @@ -663,7 +700,7 @@ describe API::Projects do end it 'sets a project as allowing merge only if all discussions are resolved' do - project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true) post api("/projects/user/#{user.id}", admin), project @@ -732,6 +769,7 @@ describe API::Projects do expect(json_response['wiki_enabled']).to be_present expect(json_response['jobs_enabled']).to be_present expect(json_response['snippets_enabled']).to be_present + expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['container_registry_enabled']).to be_present expect(json_response['created_at']).to be_present expect(json_response['last_activity_at']).to be_present diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 993164aa8fe..12720355a6d 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -557,17 +557,36 @@ describe API::Runner do { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }] end + let(:trigger) { create(:ci_trigger, project: project) } + let!(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, builds: [job], trigger: trigger) } + before do - trigger = create(:ci_trigger, project: project) - create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger) project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') end - it 'returns variables for triggers' do - request_job + shared_examples 'expected variables behavior' do + it 'returns variables for triggers' do + request_job - expect(response).to have_http_status(201) - expect(json_response['variables']).to include(*expected_variables) + expect(response).to have_http_status(201) + expect(json_response['variables']).to include(*expected_variables) + end + end + + context 'when variables are stored in trigger_request' do + before do + trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } ) + end + + it_behaves_like 'expected variables behavior' + end + + context 'when variables are stored in pipeline_variables' do + before do + create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1') + end + + it_behaves_like 'expected variables behavior' end end @@ -626,13 +645,34 @@ describe API::Runner do it 'mark job as succeeded' do update_job(state: 'success') - expect(job.reload.status).to eq 'success' + job.reload + expect(job).to be_success end it 'mark job as failed' do update_job(state: 'failed') - expect(job.reload.status).to eq 'failed' + job.reload + expect(job).to be_failed + expect(job).to be_unknown_failure + end + + context 'when failure_reason is script_failure' do + before do + update_job(state: 'failed', failure_reason: 'script_failure') + job.reload + end + + it { expect(job).to be_script_failure } + end + + context 'when failure_reason is runner_system_failure' do + before do + update_job(state: 'failed', failure_reason: 'runner_system_failure') + job.reload + end + + it { expect(job).to be_runner_system_failure } end end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index 244895a417e..67907579225 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -191,7 +191,8 @@ describe API::Runners do active: !active, tag_list: ['ruby2.1', 'pgsql', 'mysql'], run_untagged: 'false', - locked: 'true') + locked: 'true', + access_level: 'ref_protected') shared_runner.reload expect(response).to have_http_status(200) @@ -200,6 +201,7 @@ describe API::Runners do expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') expect(shared_runner.run_untagged?).to be(false) expect(shared_runner.locked?).to be(true) + expect(shared_runner.ref_protected?).to be_truthy expect(shared_runner.ensure_runner_queue_value) .not_to eq(runner_queue_value) end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5fef4437997..37cb95a16e3 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -4,6 +4,7 @@ describe API::Users do let(:user) { create(:user) } let(:admin) { create(:admin) } let(:key) { create(:key, user: user) } + let(:gpg_key) { create(:gpg_key, user: user) } let(:email) { create(:email, user: user) } let(:omniauth_user) { create(:omniauth_user) } let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } @@ -753,6 +754,164 @@ describe API::Users do end end + describe 'POST /users/:id/keys' do + before do + admin + end + + it 'does not create invalid GPG key' do + post api("/users/#{user.id}/gpg_keys", admin) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('key is missing') + end + + it 'creates GPG key' do + key_attrs = attributes_for :gpg_key + expect do + post api("/users/#{user.id}/gpg_keys", admin), key_attrs + + expect(response).to have_http_status(201) + end.to change { user.gpg_keys.count }.by(1) + end + + it 'returns 400 for invalid ID' do + post api('/users/999999/gpg_keys', admin) + + expect(response).to have_http_status(400) + end + end + + describe 'GET /user/:id/gpg_keys' do + before do + admin + end + + context 'when unauthenticated' do + it 'returns authentication error' do + get api("/users/#{user.id}/gpg_keys") + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns 404 for non-existing user' do + get api('/users/999999/gpg_keys', admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns 404 error if key not foud' do + delete api("/users/#{user.id}/gpg_keys/42", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it 'returns array of GPG keys' do + user.gpg_keys << gpg_key + user.save + + get api("/users/#{user.id}/gpg_keys", admin) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['key']).to eq(gpg_key.key) + end + end + end + + describe 'DELETE /user/:id/gpg_keys/:key_id' do + before do + admin + end + + context 'when unauthenticated' do + it 'returns authentication error' do + delete api("/users/#{user.id}/keys/42") + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'deletes existing key' do + user.gpg_keys << gpg_key + user.save + + expect do + delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin) + + expect(response).to have_http_status(204) + end.to change { user.gpg_keys.count }.by(-1) + end + + it 'returns 404 error if user not found' do + user.keys << key + user.save + + delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns 404 error if key not foud' do + delete api("/users/#{user.id}/gpg_keys/42", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + end + end + + describe 'POST /user/:id/gpg_keys/:key_id/revoke' do + before do + admin + end + + context 'when unauthenticated' do + it 'returns authentication error' do + post api("/users/#{user.id}/gpg_keys/42/revoke") + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'revokes existing key' do + user.gpg_keys << gpg_key + user.save + + expect do + post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin) + + expect(response).to have_http_status(:accepted) + end.to change { user.gpg_keys.count }.by(-1) + end + + it 'returns 404 error if user not found' do + user.gpg_keys << gpg_key + user.save + + post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 User Not Found') + end + + it 'returns 404 error if key not foud' do + post api("/users/#{user.id}/gpg_keys/42/revoke", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + end + end + describe "POST /users/:id/emails" do before do admin @@ -1153,6 +1312,173 @@ describe API::Users do end end + describe 'GET /user/gpg_keys' do + context 'when unauthenticated' do + it 'returns authentication error' do + get api('/user/gpg_keys') + + expect(response).to have_http_status(401) + end + end + + context 'when authenticated' do + it 'returns array of GPG keys' do + user.gpg_keys << gpg_key + user.save + + get api('/user/gpg_keys', user) + + expect(response).to have_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['key']).to eq(gpg_key.key) + end + + context 'scopes' do + let(:path) { '/user/gpg_keys' } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end + end + end + + describe 'GET /user/gpg_keys/:key_id' do + it 'returns a single key' do + user.gpg_keys << gpg_key + user.save + + get api("/user/gpg_keys/#{gpg_key.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['key']).to eq(gpg_key.key) + end + + it 'returns 404 Not Found within invalid ID' do + get api('/user/gpg_keys/42', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it "returns 404 error if admin accesses user's GPG key" do + user.gpg_keys << gpg_key + user.save + + get api("/user/gpg_keys/#{gpg_key.id}", admin) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it 'returns 404 for invalid ID' do + get api('/users/gpg_keys/ASDF', admin) + + expect(response).to have_http_status(404) + end + + context 'scopes' do + let(:path) { "/user/gpg_keys/#{gpg_key.id}" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end + end + + describe 'POST /user/gpg_keys' do + it 'creates a GPG key' do + key_attrs = attributes_for :gpg_key + expect do + post api('/user/gpg_keys', user), key_attrs + + expect(response).to have_http_status(201) + end.to change { user.gpg_keys.count }.by(1) + end + + it 'returns a 401 error if unauthorized' do + post api('/user/gpg_keys'), key: 'some key' + + expect(response).to have_http_status(401) + end + + it 'does not create GPG key without key' do + post api('/user/gpg_keys', user) + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('key is missing') + end + end + + describe 'POST /user/gpg_keys/:key_id/revoke' do + it 'revokes existing GPG key' do + user.gpg_keys << gpg_key + user.save + + expect do + post api("/user/gpg_keys/#{gpg_key.id}/revoke", user) + + expect(response).to have_http_status(:accepted) + end.to change { user.gpg_keys.count}.by(-1) + end + + it 'returns 404 if key ID not found' do + post api('/user/gpg_keys/42/revoke', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it 'returns 401 error if unauthorized' do + user.gpg_keys << gpg_key + user.save + + post api("/user/gpg_keys/#{gpg_key.id}/revoke") + + expect(response).to have_http_status(401) + end + + it 'returns a 404 for invalid ID' do + post api('/users/gpg_keys/ASDF/revoke', admin) + + expect(response).to have_http_status(404) + end + end + + describe 'DELETE /user/gpg_keys/:key_id' do + it 'deletes existing GPG key' do + user.gpg_keys << gpg_key + user.save + + expect do + delete api("/user/gpg_keys/#{gpg_key.id}", user) + + expect(response).to have_http_status(204) + end.to change { user.gpg_keys.count}.by(-1) + end + + it 'returns 404 if key ID not found' do + delete api('/user/gpg_keys/42', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 GPG Key Not Found') + end + + it 'returns 401 error if unauthorized' do + user.gpg_keys << gpg_key + user.save + + delete api("/user/gpg_keys/#{gpg_key.id}") + + expect(response).to have_http_status(401) + end + + it 'returns a 404 for invalid ID' do + delete api('/users/gpg_keys/ASDF', admin) + + expect(response).to have_http_status(404) + end + end + describe "GET /user/emails" do context "when unauthenticated" do it "returns authentication error" do diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb index 8fb96b3c7c5..6d0ca33a6fa 100644 --- a/spec/requests/api/v3/commits_spec.rb +++ b/spec/requests/api/v3/commits_spec.rb @@ -386,7 +386,7 @@ describe API::V3::Commits do end it "returns status for CI" do - pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha) + pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false) pipeline.update(status: 'success') get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) @@ -396,7 +396,7 @@ describe API::V3::Commits do end it "returns status for CI when pipeline is created" do - project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha) + project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false) get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb index 4ffa5d1784e..dc7f0eefd16 100644 --- a/spec/requests/api/v3/files_spec.rb +++ b/spec/requests/api/v3/files_spec.rb @@ -127,7 +127,7 @@ describe API::V3::Files do it "returns a 400 if editor fails to create file" do allow_any_instance_of(Repository).to receive(:create_file) - .and_raise(Repository::CommitError, 'Cannot create file') + .and_raise(Gitlab::Git::CommitError, 'Cannot create file') post v3_api("/projects/#{project.id}/repository/files", user), valid_params @@ -228,7 +228,7 @@ describe API::V3::Files do end it "returns a 400 if fails to delete file" do - allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file') + allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file') delete v3_api("/projects/#{project.id}/repository/files", user), valid_params diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index a514166274a..cae2c3118da 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -687,6 +687,7 @@ describe API::V3::Projects do expect(json_response['wiki_enabled']).to be_present expect(json_response['builds_enabled']).to be_present expect(json_response['snippets_enabled']).to be_present + expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['container_registry_enabled']).to be_present expect(json_response['created_at']).to be_present expect(json_response['last_activity_at']).to be_present diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb index d4648136841..7ccf387f2dc 100644 --- a/spec/requests/api/v3/triggers_spec.rb +++ b/spec/requests/api/v3/triggers_spec.rb @@ -37,7 +37,7 @@ describe API::V3::Triggers do it 'returns unauthorized if token is for different project' do post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') - expect(response).to have_http_status(401) + expect(response).to have_http_status(404) end end @@ -80,7 +80,8 @@ describe API::V3::Triggers do post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') expect(response).to have_http_status(201) pipeline.builds.reload - expect(pipeline.builds.first.trigger_request.variables).to eq(variables) + expect(pipeline.variables.map { |v| { v.key => v.value } }.first).to eq(variables) + expect(json_response['variables']).to eq(variables) end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4ba3dada37c..4775ba4c2d8 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::CreatePipelineService do - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } let(:user) { create(:admin) } let(:ref_name) { 'refs/heads/master' } @@ -391,12 +391,15 @@ describe Ci::CreatePipelineService do end context 'when user is master' do + let(:pipeline) { execute_service } + before do project.add_master(user) end - it 'creates a pipeline' do - expect(execute_service).to be_persisted + it 'creates a protected pipeline' do + expect(pipeline).to be_persisted + expect(pipeline).to be_protected expect(Ci::Pipeline.count).to eq(1) end end @@ -468,10 +471,11 @@ describe Ci::CreatePipelineService do let(:user) {} let(:trigger) { create(:ci_trigger, owner: nil) } let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) } + let(:pipeline) { execute_service(trigger_request: trigger_request) } - it 'creates a pipeline' do - expect(execute_service(trigger_request: trigger_request)) - .to be_persisted + it 'creates an unprotected pipeline' do + expect(pipeline).to be_persisted + expect(pipeline).not_to be_protected expect(Ci::Pipeline.count).to eq(1) end end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb deleted file mode 100644 index 8295813a1ca..00000000000 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'spec_helper' - -describe Ci::CreateTriggerRequestService do - let(:service) { described_class } - let(:project) { create(:project, :repository) } - let(:trigger) { create(:ci_trigger, project: project, owner: owner) } - let(:owner) { create(:user) } - - before do - stub_ci_pipeline_to_return_yaml_file - - project.add_developer(owner) - end - - describe '#execute' do - context 'valid params' do - subject { service.execute(project, trigger, 'master') } - - context 'without owner' do - it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) } - it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) } - it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } - it { expect(subject.pipeline).to be_trigger } - end - - context 'with owner' do - it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) } - it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) } - it { expect(subject.trigger_request.builds.first.user).to eq(owner) } - it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } - it { expect(subject.pipeline).to be_trigger } - it { expect(subject.pipeline.user).to eq(owner) } - end - end - - context 'no commit for ref' do - subject { service.execute(project, trigger, 'other-branch') } - - it { expect(subject.pipeline).not_to be_persisted } - end - - context 'no builds created' do - subject { service.execute(project, trigger, 'master') } - - before do - stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }') - end - - it { expect(subject.pipeline).not_to be_persisted } - end - end -end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 8eb0d2d10a4..5ac30111ec9 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -4,7 +4,7 @@ module Ci describe RegisterJobService do let!(:project) { FactoryGirl.create :project, shared_runners_enabled: false } let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } - let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline } + let!(:pending_job) { FactoryGirl.create :ci_build, pipeline: pipeline } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } @@ -15,32 +15,32 @@ module Ci describe '#execute' do context 'runner follow tag list' do it "picks build with the same tag" do - pending_build.tag_list = ["linux"] - pending_build.save + pending_job.tag_list = ["linux"] + pending_job.save specific_runner.tag_list = ["linux"] - expect(execute(specific_runner)).to eq(pending_build) + expect(execute(specific_runner)).to eq(pending_job) end it "does not pick build with different tag" do - pending_build.tag_list = ["linux"] - pending_build.save + pending_job.tag_list = ["linux"] + pending_job.save specific_runner.tag_list = ["win32"] expect(execute(specific_runner)).to be_falsey end it "picks build without tag" do - expect(execute(specific_runner)).to eq(pending_build) + expect(execute(specific_runner)).to eq(pending_job) end it "does not pick build with tag" do - pending_build.tag_list = ["linux"] - pending_build.save + pending_job.tag_list = ["linux"] + pending_job.save expect(execute(specific_runner)).to be_falsey end it "pick build without tag" do specific_runner.tag_list = ["win32"] - expect(execute(specific_runner)).to eq(pending_build) + expect(execute(specific_runner)).to eq(pending_job) end end @@ -76,7 +76,7 @@ module Ci let!(:pipeline2) { create :ci_pipeline, project: project2 } let!(:project3) { create :project, shared_runners_enabled: true } let!(:pipeline3) { create :ci_pipeline, project: project3 } - let!(:build1_project1) { pending_build } + let!(:build1_project1) { pending_job } let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline } let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline } let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 } @@ -172,7 +172,7 @@ module Ci context 'when first build is stalled' do before do - pending_build.lock_version = 10 + pending_job.lock_version = 10 end subject { described_class.new(specific_runner).execute } @@ -182,7 +182,7 @@ module Ci before do allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner) - .and_return([pending_build, other_build]) + .and_return([pending_job, other_build]) end it "receives second build from the queue" do @@ -194,7 +194,7 @@ module Ci context 'when single build is in queue' do before do allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner) - .and_return([pending_build]) + .and_return([pending_job]) end it "does not receive any valid result" do @@ -215,6 +215,70 @@ module Ci end end + context 'when access_level of runner is not_protected' do + let!(:specific_runner) { create(:ci_runner, :specific) } + + context 'when a job is protected' do + let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) } + + it 'picks the job' do + expect(execute(specific_runner)).to eq(pending_job) + end + end + + context 'when a job is unprotected' do + let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + + it 'picks the job' do + expect(execute(specific_runner)).to eq(pending_job) + end + end + + context 'when protected attribute of a job is nil' do + let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + + before do + pending_job.update_attribute(:protected, nil) + end + + it 'picks the job' do + expect(execute(specific_runner)).to eq(pending_job) + end + end + end + + context 'when access_level of runner is ref_protected' do + let!(:specific_runner) { create(:ci_runner, :ref_protected, :specific) } + + context 'when a job is protected' do + let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) } + + it 'picks the job' do + expect(execute(specific_runner)).to eq(pending_job) + end + end + + context 'when a job is unprotected' do + let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + + it 'does not pick the job' do + expect(execute(specific_runner)).to be_nil + end + end + + context 'when protected attribute of a job is nil' do + let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + + before do + pending_job.update_attribute(:protected, nil) + end + + it 'does not pick the job' do + expect(execute(specific_runner)).to be_nil + end + end + end + def execute(runner) described_class.new(runner).execute.build end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index cec667071cc..bbc3a8c79f5 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -22,7 +22,7 @@ describe Ci::RetryBuildService do %i[type lock_version target_url base_tags commit_id deployments erased_by_id last_deployment project_id runner_id tag_taggings taggings tags trigger_request_id - user_id auto_canceled_by_id retried].freeze + user_id auto_canceled_by_id retried failure_reason].freeze shared_examples 'build duplication' do let(:stage) do @@ -48,10 +48,21 @@ describe Ci::RetryBuildService do describe 'clone accessors' do CLONE_ACCESSORS.each do |attribute| it "clones #{attribute} build attribute" do - expect(new_build.send(attribute)).to be_present + expect(new_build.send(attribute)).not_to be_nil expect(new_build.send(attribute)).to eq build.send(attribute) end end + + context 'when job has nullified protected' do + before do + build.update_attribute(:protected, nil) + end + + it "clones protected build attribute" do + expect(new_build.protected).to be_nil + expect(new_build.protected).to eq build.protected + end + end end describe 'reject acessors' do diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb index c239494298b..82b156f5ebe 100644 --- a/spec/services/discussions/update_diff_position_service_spec.rb +++ b/spec/services/discussions/update_diff_position_service_spec.rb @@ -150,21 +150,7 @@ describe Discussions::UpdateDiffPositionService do ) end - context "when the diff line is the same" do - let(:line) { 16 } - - it "updates the position" do - subject.execute(discussion) - - expect(discussion.original_position).to eq(old_position) - expect(discussion.position).not_to eq(old_position) - expect(discussion.position.new_line).to eq(22) - end - end - - context "when the diff line has changed" do - let(:line) { 9 } - + shared_examples 'outdated diff note' do it "doesn't update the position" do subject.execute(discussion) @@ -189,5 +175,51 @@ describe Discussions::UpdateDiffPositionService do subject.execute(discussion) end end + + context "when the diff line is the same" do + let(:line) { 16 } + + it "updates the position" do + subject.execute(discussion) + + expect(discussion.original_position).to eq(old_position) + expect(discussion.position).not_to eq(old_position) + expect(discussion.position.new_line).to eq(22) + end + + context 'when the resolve_outdated_diff_discussions setting is set' do + before do + project.update!(resolve_outdated_diff_discussions: true) + end + + it 'does not resolve the discussion' do + subject.execute(discussion) + + expect(discussion).not_to be_resolved + expect(discussion).not_to be_resolved_by_push + end + end + end + + context "when the diff line has changed" do + let(:line) { 9 } + + include_examples 'outdated diff note' + + context 'when the resolve_outdated_diff_discussions setting is set' do + before do + project.update!(resolve_outdated_diff_discussions: true) + end + + it 'sets resolves the discussion and sets resolved_by_push' do + subject.execute(discussion) + + expect(discussion).to be_resolved + expect(discussion).to be_resolved_by_push + end + + include_examples 'outdated diff note' + end + end end end diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index aa6ad6340f5..031366d1825 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -116,6 +116,7 @@ describe Projects::UpdatePagesService do expect(deploy_status.description) .to match(/artifacts for pages are too large/) + expect(deploy_status).to be_script_failure end end diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 39586d37e93..934b4557ba2 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -80,7 +80,8 @@ module CycleAnalyticsHelpers sha: project.repository.commit('master').sha, ref: 'master', source: :push, - project: project) + project: project, + protected: false) end def new_dummy_job(environment) @@ -93,7 +94,8 @@ module CycleAnalyticsHelpers ref: 'master', tag: false, name: 'dummy', - pipeline: dummy_pipeline) + pipeline: dummy_pipeline, + protected: false) end end diff --git a/spec/support/group_members_shared_example.rb b/spec/support/group_members_shared_example.rb new file mode 100644 index 00000000000..547c83c7955 --- /dev/null +++ b/spec/support/group_members_shared_example.rb @@ -0,0 +1,27 @@ +RSpec.shared_examples 'members and requesters associations' do + describe '#members_and_requesters' do + it 'includes members and requesters' do + member_and_requester_user_ids = namespace.members_and_requesters.pluck(:user_id) + + expect(member_and_requester_user_ids).to include(requester.id, developer.id) + end + end + + describe '#members' do + it 'includes members and exclude requesters' do + member_user_ids = namespace.members.pluck(:user_id) + + expect(member_user_ids).to include(developer.id) + expect(member_user_ids).not_to include(requester.id) + end + end + + describe '#requesters' do + it 'does not include requesters' do + requester_user_ids = namespace.requesters.pluck(:user_id) + + expect(requester_user_ids).to include(requester.id) + expect(requester_user_ids).not_to include(developer.id) + end + end +end diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 8731847592b..11ef1fc477f 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -41,7 +41,7 @@ module SeedHelper end def create_mutable_seeds - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), chdir: SEED_STORAGE_PATH, out: '/dev/null', err: '/dev/null') diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 1e39f80699c..71b9deeabc3 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -5,7 +5,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { - 'signed-commits' => '5d4a1cb', + 'signed-commits' => '2d1096e', 'not-merged-branch' => 'b83d6e3', 'branch-merged' => '498214d', 'empty-branch' => '7efb185', @@ -176,6 +176,24 @@ module TestEnv spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s @gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i } + wait_gitaly + end + + def wait_gitaly + sleep_time = 10 + sleep_interval = 0.1 + socket = Gitlab::GitalyClient.address('default').sub('unix:', '') + + Integer(sleep_time / sleep_interval).times do + begin + Socket.unix(socket) + return + rescue + sleep sleep_interval + end + end + + raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds" end def stop_gitaly diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb index 3390ae247ff..f2c19c7642a 100644 --- a/spec/views/ci/lints/show.html.haml_spec.rb +++ b/spec/views/ci/lints/show.html.haml_spec.rb @@ -73,8 +73,8 @@ describe 'ci/lints/show' do render expect(rendered).to have_content('Tag list: dotnet') - expect(rendered).to have_content('Refs only: test@dude/repo') - expect(rendered).to have_content('Refs except: deploy') + expect(rendered).to have_content('Only policy: refs, test@dude/repo') + expect(rendered).to have_content('Except policy: refs, deploy') expect(rendered).to have_content('Environment: testing') expect(rendered).to have_content('When: on_success') end diff --git a/spec/views/layouts/nav/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index faea2505e40..b17bc6692f3 100644 --- a/spec/views/layouts/nav/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -1,11 +1,13 @@ require 'spec_helper' -describe 'layouts/nav/_project' do +describe 'layouts/nav/sidebar/_project' do describe 'container registry tab' do before do + project = create(:project, :repository) stub_container_registry_config(enabled: true) - assign(:project, create(:project, :repository)) + assign(:project, project) + assign(:repository, project.repository) allow(view).to receive(:current_ref).and_return('master') allow(view).to receive(:can?).and_return(true) diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index 117f48450e2..d4279626e75 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -195,20 +195,4 @@ describe 'projects/jobs/show' do text: /\A\n#{Regexp.escape(commit_title)}\n\Z/) end end - - describe 'shows trigger variables in sidebar' do - let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline) } - - before do - build.trigger_request = trigger_request - render - end - - it 'shows trigger variables in separate lines' do - expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_1') - expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_2') - expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_1') - expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') - end - end end diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb index 54978baca88..aa6c347d738 100644 --- a/spec/workers/create_gpg_signature_worker_spec.rb +++ b/spec/workers/create_gpg_signature_worker_spec.rb @@ -7,9 +7,14 @@ describe CreateGpgSignatureWorker do let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } it 'calls Gitlab::Gpg::Commit#signature' do - expect(Gitlab::Gpg::Commit).to receive(:new).with(project, commit_sha).and_call_original + commit = instance_double(Commit) + gpg_commit = instance_double(Gitlab::Gpg::Commit) - expect_any_instance_of(Gitlab::Gpg::Commit).to receive(:signature) + allow(Project).to receive(:find_by).with(id: project.id).and_return(project) + allow(project).to receive(:commit).with(commit_sha).and_return(commit) + + expect(Gitlab::Gpg::Commit).to receive(:new).with(commit).and_return(gpg_commit) + expect(gpg_commit).to receive(:signature) described_class.new.perform(commit_sha, project.id) end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 05f971dfd13..c4979792194 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -23,8 +23,8 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original - expect_any_instance_of(Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id) end @@ -143,7 +143,7 @@ describe GitGarbageCollectWorker do tree: old_commit.tree, parents: [old_commit] ) - GitOperationService.new(nil, project.repository).send( + Gitlab::Git::OperationService.new(nil, project.repository.raw_repository).send( :update_ref, "refs/heads/#{SecureRandom.hex(6)}", new_commit_sha, diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index 549635f7f33..ac6f4fefb4e 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -6,27 +6,31 @@ describe StuckCiJobsWorker do let(:worker) { described_class.new } let(:exclusive_lease_uuid) { SecureRandom.uuid } - subject do - job.reload - job.status - end - before do job.update!(status: status, updated_at: updated_at) allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid) end shared_examples 'job is dropped' do - it 'changes status' do + before do worker.perform - is_expected.to eq('failed') + job.reload + end + + it "changes status" do + expect(job).to be_failed + expect(job).to be_stuck_or_timeout_failure end end shared_examples 'job is unchanged' do - it "doesn't change status" do + before do worker.perform - is_expected.to eq(status) + job.reload + end + + it "doesn't change status" do + expect(job.status).to eq(status) end end |