diff options
Diffstat (limited to 'spec')
114 files changed, 10086 insertions, 2874 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/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/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 25ec63de94a..c2b59239af9 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -107,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| 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/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/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/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/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_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/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/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/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 9921ca1af33..68540dd4e59 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -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/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/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/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/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 4cfb4b7d357..08959e7bc16 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -916,27 +916,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) + expect(branch).to eq(nil) + end 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 enabled' do + it_behaves_like 'finding a branch' + end - repository.find_branch('master') - branch = repository.find_branch('master', force_reload: true) + context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'finding a branch' - expect(branch).to be_a_kind_of(Gitlab::Git::Branch) - expect(branch.name).to eq('master') + 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) + + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end end end 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 8da02b0cf00..beed4e77e8b 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -264,6 +264,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 27f2ce60084..122b8ee0314 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: @@ -278,6 +279,7 @@ CommitStatus: - auto_canceled_by_id - retried - protected +- failure_reason Ci::Variable: - id - project_id @@ -397,6 +399,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 @@ -405,6 +408,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/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/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 3fe3ec17d36..08d22f166e4 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1492,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 @@ -1504,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 diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b84e3ff18e8..84656ffe0b9 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) } 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..37f6fd3a25b 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 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_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 be1ae295f75..1f7c6a82b91 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -74,6 +74,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 @@ -90,22 +91,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 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..fd83a58ed9f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2102,4 +2102,18 @@ 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 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 cc71865e1f3..e4c73583545 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -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/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/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/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/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/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_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/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 7c9c117bf71..f5ed9ff608f 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 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/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/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 |