diff options
Diffstat (limited to 'spec/lib')
212 files changed, 8175 insertions, 1868 deletions
diff --git a/spec/lib/api/entities/merge_request_basic_spec.rb b/spec/lib/api/entities/merge_request_basic_spec.rb index 715fcf4bcdb..fe4c27b70ae 100644 --- a/spec/lib/api/entities/merge_request_basic_spec.rb +++ b/spec/lib/api/entities/merge_request_basic_spec.rb @@ -40,4 +40,31 @@ RSpec.describe ::API::Entities::MergeRequestBasic do expect(batch.count).to be_within(3 * query.count).of(control.count) end end + + context 'reviewers' do + context "when merge_request_reviewers FF is enabled" do + before do + stub_feature_flags(merge_request_reviewers: true) + merge_request.reviewers = [user] + end + + it 'includes assigned reviewers' do + result = Gitlab::Json.parse(present(merge_request).to_json) + + expect(result['reviewers'][0]['username']).to eq user.username + end + end + + context "when merge_request_reviewers FF is disabled" do + before do + stub_feature_flags(merge_request_reviewers: false) + end + + it 'does not include reviewers' do + result = Gitlab::Json.parse(present(merge_request).to_json) + + expect(result.keys).not_to include('reviewers') + end + end + end end diff --git a/spec/lib/api/helpers/sse_helpers_spec.rb b/spec/lib/api/helpers/sse_helpers_spec.rb new file mode 100644 index 00000000000..397051d9142 --- /dev/null +++ b/spec/lib/api/helpers/sse_helpers_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Helpers::SSEHelpers do + include Gitlab::Routing + + let_it_be(:project) { create(:project) } + + subject { Class.new.include(described_class).new } + + describe '#request_from_sse?' do + before do + allow(subject).to receive(:request).and_return(request) + end + + context 'when referer is nil' do + let(:request) { double(referer: nil)} + + it 'returns false' do + expect(URI).not_to receive(:parse) + expect(subject.request_from_sse?(project)).to eq false + end + end + + context 'when referer is not from SSE' do + let(:request) { double(referer: 'https://gitlab.com')} + + it 'returns false' do + expect(URI).to receive(:parse).and_call_original + expect(subject.request_from_sse?(project)).to eq false + end + end + + context 'when referer is from SSE' do + let(:request) { double(referer: project_show_sse_path(project, 'master/README.md'))} + + it 'returns true' do + expect(URI).to receive(:parse).and_call_original + expect(subject.request_from_sse?(project)).to eq true + end + end + end +end diff --git a/spec/lib/api/validations/validators/integer_or_custom_value_spec.rb b/spec/lib/api/validations/validators/integer_or_custom_value_spec.rb new file mode 100644 index 00000000000..a04917736db --- /dev/null +++ b/spec/lib/api/validations/validators/integer_or_custom_value_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Validations::Validators::IntegerOrCustomValue do + include ApiValidatorsHelpers + + let(:custom_values) { %w[None Any Started Current] } + + subject { described_class.new(['test'], { values: custom_values }, false, scope.new) } + + context 'valid parameters' do + it 'does not raise a validation error' do + expect_no_validation_error('test' => 2) + expect_no_validation_error('test' => 100) + expect_no_validation_error('test' => 'None') + expect_no_validation_error('test' => 'Any') + expect_no_validation_error('test' => 'none') + expect_no_validation_error('test' => 'any') + expect_no_validation_error('test' => 'started') + expect_no_validation_error('test' => 'CURRENT') + end + + context 'when custom values is empty and value is an integer' do + let(:custom_values) { [] } + + it 'does not raise a validation error' do + expect_no_validation_error({ 'test' => 5 }) + end + end + end + + context 'invalid parameters' do + it 'raises a validation error' do + expect_validation_error({ 'test' => 'Upcomming' }) + end + + context 'when custom values is empty and value is not an integer' do + let(:custom_values) { [] } + + it 'raises a validation error' do + expect_validation_error({ 'test' => '5' }) + end + end + end +end diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb index cefd1fa3274..6a161854dfb 100644 --- a/spec/lib/atlassian/jira_connect/client_spec.rb +++ b/spec/lib/atlassian/jira_connect/client_spec.rb @@ -7,8 +7,10 @@ RSpec.describe Atlassian::JiraConnect::Client do subject { described_class.new('https://gitlab-test.atlassian.net', 'sample_secret') } + let_it_be(:project) { create_default(:project, :repository) } + around do |example| - Timecop.freeze { example.run } + freeze_time { example.run } end describe '.generate_update_sequence_id' do @@ -19,41 +21,158 @@ RSpec.describe Atlassian::JiraConnect::Client do end end - describe '#store_dev_info' do - let_it_be(:project) { create_default(:project, :repository) } - let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches) } + describe '#send_info' do + it 'calls store_build_info and store_dev_info as appropriate' do + expect(subject).to receive(:store_build_info).with( + project: project, + update_sequence_id: :x, + pipelines: :y + ).and_return(:build_stored) + + expect(subject).to receive(:store_dev_info).with( + project: project, + update_sequence_id: :x, + commits: :a, + branches: :b, + merge_requests: :c + ).and_return(:dev_stored) + + args = { + project: project, + update_sequence_id: :x, + commits: :a, + branches: :b, + merge_requests: :c, + pipelines: :y + } + + expect(subject.send_info(**args)).to contain_exactly(:dev_stored, :build_stored) + end - let(:expected_jwt) do - Atlassian::Jwt.encode( - Atlassian::Jwt.build_claims( - Atlassian::JiraConnect.app_key, - '/rest/devinfo/0.10/bulk', - 'POST' - ), - 'sample_secret' - ) + it 'only calls methods that we need to call' do + expect(subject).to receive(:store_dev_info).with( + project: project, + update_sequence_id: :x, + commits: :a + ).and_return(:dev_stored) + + args = { + project: project, + update_sequence_id: :x, + commits: :a + } + + expect(subject.send_info(**args)).to contain_exactly(:dev_stored) + end + + it 'raises an argument error if there is nothing to send (probably a typo?)' do + expect { subject.send_info(project: project, builds: :x) } + .to raise_error(ArgumentError) + end + end + + def expected_headers(path) + expected_jwt = Atlassian::Jwt.encode( + Atlassian::Jwt.build_claims(Atlassian::JiraConnect.app_key, path, 'POST'), + 'sample_secret' + ) + + { + 'Authorization' => "JWT #{expected_jwt}", + 'Content-Type' => 'application/json' + } + end + + describe '#store_build_info' do + let_it_be(:mrs_by_title) { create_list(:merge_request, 4, :unique_branches, :jira_title) } + let_it_be(:mrs_by_branch) { create_list(:merge_request, 2, :jira_branch) } + let_it_be(:red_herrings) { create_list(:merge_request, 1, :unique_branches) } + + let_it_be(:pipelines) do + (red_herrings + mrs_by_branch + mrs_by_title).map do |mr| + create(:ci_pipeline, merge_request: mr) + end + end + + let(:build_info_payload_schema) do + Atlassian::Schemata.build_info_payload + end + + let(:body) do + matcher = be_valid_json.according_to_schema(build_info_payload_schema) + + ->(text) { matcher.matches?(text) } end before do - stub_full_request('https://gitlab-test.atlassian.net/rest/devinfo/0.10/bulk', method: :post) - .with( - headers: { - 'Authorization' => "JWT #{expected_jwt}", - 'Content-Type' => 'application/json' - } - ) + path = '/rest/builds/0.1/bulk' + stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post) + .with(body: body, headers: expected_headers(path)) + end + + it "calls the API with auth headers" do + subject.send(:store_build_info, project: project, pipelines: pipelines) + end + + it 'only sends information about relevant MRs' do + expect(subject).to receive(:post).with('/rest/builds/0.1/bulk', { builds: have_attributes(size: 6) }) + + subject.send(:store_build_info, project: project, pipelines: pipelines) + end + + it 'does not call the API if there is nothing to report' do + expect(subject).not_to receive(:post) + + subject.send(:store_build_info, project: project, pipelines: pipelines.take(1)) + end + + it 'does not call the API if the feature flag is not enabled' do + stub_feature_flags(jira_sync_builds: false) + + expect(subject).not_to receive(:post) + + subject.send(:store_build_info, project: project, pipelines: pipelines) + end + + it 'does call the API if the feature flag enabled for the project' do + stub_feature_flags(jira_sync_builds: project) + + expect(subject).to receive(:post).with('/rest/builds/0.1/bulk', { builds: Array }) + + subject.send(:store_build_info, project: project, pipelines: pipelines) + end + + it 'avoids N+1 database queries' do + baseline = ActiveRecord::QueryRecorder.new do + subject.send(:store_build_info, project: project, pipelines: pipelines) + end + + pipelines << create(:ci_pipeline, head_pipeline_of: create(:merge_request, :jira_branch)) + + expect { subject.send(:store_build_info, project: project, pipelines: pipelines) }.not_to exceed_query_limit(baseline) + end + end + + describe '#store_dev_info' do + let_it_be(:merge_requests) { create_list(:merge_request, 2, :unique_branches) } + + before do + path = '/rest/devinfo/0.10/bulk' + + stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post) + .with(headers: expected_headers(path)) end it "calls the API with auth headers" do - subject.store_dev_info(project: project) + subject.send(:store_dev_info, project: project) end it 'avoids N+1 database queries' do - control_count = ActiveRecord::QueryRecorder.new { subject.store_dev_info(project: project, merge_requests: merge_requests) }.count + control_count = ActiveRecord::QueryRecorder.new { subject.send(:store_dev_info, project: project, merge_requests: merge_requests) }.count merge_requests << create(:merge_request, :unique_branches) - expect { subject.store_dev_info(project: project, merge_requests: merge_requests) }.not_to exceed_query_limit(control_count) + expect { subject.send(:store_dev_info, project: project, merge_requests: merge_requests) }.not_to exceed_query_limit(control_count) end end end diff --git a/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb new file mode 100644 index 00000000000..52e475d20ca --- /dev/null +++ b/spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Atlassian::JiraConnect::Serializers::BuildEntity do + let_it_be(:user) { create_default(:user) } + let_it_be(:project) { create_default(:project) } + + subject { described_class.represent(pipeline) } + + context 'when the pipeline does not belong to any Jira issue' do + let_it_be(:pipeline) { create(:ci_pipeline) } + + describe '#issue_keys' do + it 'is empty' do + expect(subject.issue_keys).to be_empty + end + end + + describe '#to_json' do + it 'can encode the object' do + expect(subject.to_json).to be_valid_json + end + + it 'is invalid, since it has no issue keys' do + expect(subject.to_json).not_to be_valid_json.according_to_schema(Atlassian::Schemata.build_info) + end + end + end + + context 'when the pipeline does belong to a Jira issue' do + let(:pipeline) { create(:ci_pipeline, merge_request: merge_request) } + + %i[jira_branch jira_title].each do |trait| + context "because it belongs to an MR with a #{trait}" do + let(:merge_request) { create(:merge_request, trait) } + + describe '#issue_keys' do + it 'is not empty' do + expect(subject.issue_keys).not_to be_empty + end + end + + describe '#to_json' do + it 'is valid according to the build info schema' do + expect(subject.to_json).to be_valid_json.according_to_schema(Atlassian::Schemata.build_info) + end + end + end + end + end +end diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb index dbc04704fba..450e396a389 100644 --- a/spec/lib/backup/files_spec.rb +++ b/spec/lib/backup/files_spec.rb @@ -149,13 +149,27 @@ RSpec.describe Backup::Files do end it 'excludes tmp dirs from rsync' do - expect(Gitlab::Popen).to receive(:popen).with(%w(rsync -a --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)).and_return(['', 0]) + expect(Gitlab::Popen).to receive(:popen) + .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) + .and_return(['', 0]) subject.dump end + it 'retries if rsync fails due to vanishing files' do + expect(Gitlab::Popen).to receive(:popen) + .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) + .and_return(['rsync failed', 24], ['', 0]) + + expect do + subject.dump + end.to output(/files vanished during rsync, retrying/).to_stdout + end + it 'raises an error and outputs an error message if rsync failed' do - allow(Gitlab::Popen).to receive(:popen).with(%w(rsync -a --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)).and_return(['rsync failed', 1]) + allow(Gitlab::Popen).to receive(:popen) + .with(%w(rsync -a --delete --exclude=lost+found --exclude=/@pages.tmp /var/gitlab-pages /var/gitlab-backup)) + .and_return(['rsync failed', 1]) expect do subject.dump diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb index 9c139e9f954..492058c6a00 100644 --- a/spec/lib/backup/repositories_spec.rb +++ b/spec/lib/backup/repositories_spec.rb @@ -242,7 +242,9 @@ RSpec.describe Backup::Repositories do # 4 times = project repo + wiki repo + project_snippet repo + personal_snippet repo expect(Repository).to receive(:new).exactly(4).times.and_wrap_original do |method, *original_args| - repository = method.call(*original_args) + full_path, container, kwargs = original_args + + repository = method.call(full_path, container, **kwargs) expect(repository).to receive(:remove) diff --git a/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb b/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb new file mode 100644 index 00000000000..272b4386ec8 --- /dev/null +++ b/spec/lib/banzai/filter/ascii_doc_sanitization_filter_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::AsciiDocSanitizationFilter do + include FilterSpecHelper + + it 'preserves footnotes refs' do + result = filter('<p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>').to_html + expect(result).to eq('<p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>') + end + + it 'preserves footnotes defs' do + result = filter('<div id="_footnotedef_1"> +<a href="#_footnoteref_1">1</a>. This is the text of the footnote.</div>').to_html + expect(result).to eq(%(<div id="_footnotedef_1"> +<a href="#_footnoteref_1">1</a>. This is the text of the footnote.</div>)) + end + + it 'preserves user-content- prefixed ids on anchors' do + result = filter('<p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document.</p>').to_html + expect(result).to eq(%(<p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document.</p>)) + end + + it 'preserves user-content- prefixed ids on div (blocks)' do + html_content = <<~HTML + <div id="user-content-open-block" class="openblock"> + <div class="content"> + <div class="paragraph"> + <p>This is an open block</p> + </div> + </div> + </div> + HTML + output = <<~SANITIZED_HTML + <div id="user-content-open-block"> + <div> + <div> + <p>This is an open block</p> + </div> + </div> + </div> + SANITIZED_HTML + expect(filter(html_content).to_html).to eq(output) + end + + it 'preserves section anchor ids' do + result = filter(%(<h2 id="user-content-first-section"> +<a class="anchor" href="#user-content-first-section"></a>First section</h2>)).to_html + expect(result).to eq(%(<h2 id="user-content-first-section"> +<a class="anchor" href="#user-content-first-section"></a>First section</h2>)) + end + + it 'removes non prefixed ids' do + result = filter('<p><a id="cross-references"></a>A link to another location within an AsciiDoc document.</p>').to_html + expect(result).to eq(%(<p><a></a>A link to another location within an AsciiDoc document.</p>)) + end +end diff --git a/spec/lib/banzai/filter/kroki_filter_spec.rb b/spec/lib/banzai/filter/kroki_filter_spec.rb new file mode 100644 index 00000000000..57caba1d4d7 --- /dev/null +++ b/spec/lib/banzai/filter/kroki_filter_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::KrokiFilter do + include FilterSpecHelper + + it 'replaces nomnoml pre tag with img tag if kroki is enabled' do + stub_application_setting(kroki_enabled: true, kroki_url: "http://localhost:8000") + doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") + + expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==">' + end + + it 'replaces nomnoml pre tag with img tag if both kroki and plantuml are enabled' do + stub_application_setting(kroki_enabled: true, + kroki_url: "http://localhost:8000", + plantuml_enabled: true, + plantuml_url: "http://localhost:8080") + doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") + + expect(doc.to_s).to eq '<img src="http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==">' + end + + it 'does not replace nomnoml pre tag with img tag if kroki is disabled' do + stub_application_setting(kroki_enabled: false) + doc = filter("<pre lang='nomnoml'><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>") + + expect(doc.to_s).to eq "<pre lang=\"nomnoml\"><code>[Pirate|eyeCount: Int|raid();pillage()|\n [beard]--[parrot]\n [beard]-:>[foul mouth]\n]</code></pre>" + end + + it 'does not replace plantuml pre tag with img tag if both kroki and plantuml are enabled' do + stub_application_setting(kroki_enabled: true, + kroki_url: "http://localhost:8000", + plantuml_enabled: true, + plantuml_url: "http://localhost:8080") + doc = filter("<pre lang='plantuml'><code>Bob->Alice : hello</code></pre>") + + expect(doc.to_s).to eq '<pre lang="plantuml"><code>Bob->Alice : hello</code></pre>' + end +end diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb index 8d01a651651..c5e84a0c1e7 100644 --- a/spec/lib/banzai/filter/markdown_filter_spec.rb +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -46,6 +46,12 @@ RSpec.describe Banzai::Filter::MarkdownFilter do expect(result).to start_with('<pre><code lang="日">') end + + it 'works with additional language parameters' do + result = filter("```ruby:red gem\nsome code\n```", no_sourcepos: true) + + expect(result).to start_with('<pre><code lang="ruby:red gem">') + end end end diff --git a/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb b/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb index d8841a9753e..74005adf673 100644 --- a/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/jira_import/adf_commonmark_pipeline_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Banzai::Pipeline::JiraImport::AdfCommonmarkPipeline do let_it_be(:fixtures_path) { 'lib/kramdown/atlassian_document_format' } - it 'converts text in Atlassian Document Format ' do + it 'converts text in Atlassian Document Format' do source = fixture_file(File.join(fixtures_path, 'paragraph.json')) target = fixture_file(File.join(fixtures_path, 'paragraph.md')) output = described_class.call(source, {})[:output] diff --git a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb index cde8e2d5c18..a7a19fb73fc 100644 --- a/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb +++ b/spec/lib/bulk_imports/common/extractors/graphql_extractor_spec.rb @@ -41,12 +41,11 @@ RSpec.describe BulkImports::Common::Extractors::GraphqlExtractor do end context 'when variables are present' do - let(:query) { { query: double(to_s: 'test', variables: { full_path: :source_full_path }) } } + let(:variables) { { foo: :bar } } + let(:query) { { query: double(to_s: 'test', variables: variables) } } it 'builds graphql query variables for import entity' do - expected_variables = { full_path: import_entity.source_full_path } - - expect(graphql_client).to receive(:execute).with(anything, expected_variables) + expect(graphql_client).to receive(:execute).with(anything, variables) subject.extract(context).first end diff --git a/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb deleted file mode 100644 index 8f39b6e7c93..00000000000 --- a/spec/lib/bulk_imports/common/transformers/graphql_cleaner_transformer_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe BulkImports::Common::Transformers::GraphqlCleanerTransformer do - describe '#transform' do - let_it_be(:expected_output) do - { - 'name' => 'test', - 'fullName' => 'test', - 'description' => 'test', - 'labels' => [ - { 'title' => 'label1' }, - { 'title' => 'label2' }, - { 'title' => 'label3' } - ] - } - end - - it 'deep cleans hash from GraphQL keys' do - data = { - 'data' => { - 'group' => { - 'name' => 'test', - 'fullName' => 'test', - 'description' => 'test', - 'labels' => { - 'edges' => [ - { 'node' => { 'title' => 'label1' } }, - { 'node' => { 'title' => 'label2' } }, - { 'node' => { 'title' => 'label3' } } - ] - } - } - } - } - - transformed_data = described_class.new.transform(nil, data) - - expect(transformed_data).to eq(expected_output) - end - - context 'when data does not have data/group nesting' do - it 'deep cleans hash from GraphQL keys' do - data = { - 'name' => 'test', - 'fullName' => 'test', - 'description' => 'test', - 'labels' => { - 'edges' => [ - { 'node' => { 'title' => 'label1' } }, - { 'node' => { 'title' => 'label2' } }, - { 'node' => { 'title' => 'label3' } } - ] - } - } - - transformed_data = described_class.new.transform(nil, data) - - expect(transformed_data).to eq(expected_output) - end - end - - context 'when data is not a hash' do - it 'does not perform transformation' do - data = 'test' - - transformed_data = described_class.new.transform(nil, data) - - expect(transformed_data).to eq(data) - end - end - - context 'when nested data is not an array or hash' do - it 'only removes top level data/group keys' do - data = { - 'data' => { - 'group' => 'test' - } - } - - transformed_data = described_class.new.transform(nil, data) - - expect(transformed_data).to eq('test') - end - end - end -end diff --git a/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb b/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb new file mode 100644 index 00000000000..2b33701653e --- /dev/null +++ b/spec/lib/bulk_imports/common/transformers/hash_key_digger_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Common::Transformers::HashKeyDigger do + describe '#transform' do + it 'when the key_path is an array' do + data = { foo: { bar: :value } } + key_path = %i[foo bar] + transformed = described_class.new(key_path: key_path).transform(nil, data) + + expect(transformed).to eq(:value) + end + + it 'when the key_path is not an array' do + data = { foo: { bar: :value } } + key_path = :foo + transformed = described_class.new(key_path: key_path).transform(nil, data) + + expect(transformed).to eq({ bar: :value }) + end + + it "when the data is not a hash" do + expect { described_class.new(key_path: nil).transform(nil, nil) } + .to raise_error(ArgumentError, "Given data must be a Hash") + end + end +end diff --git a/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb new file mode 100644 index 00000000000..03d138b227c --- /dev/null +++ b/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Common::Transformers::ProhibitedAttributesTransformer do + describe '#transform' do + let_it_be(:hash) do + { + 'id' => 101, + 'service_id' => 99, + 'moved_to_id' => 99, + 'namespace_id' => 99, + 'ci_id' => 99, + 'random_project_id' => 99, + 'random_id' => 99, + 'milestone_id' => 99, + 'project_id' => 99, + 'user_id' => 99, + 'random_id_in_the_middle' => 99, + 'notid' => 99, + 'import_source' => 'test', + 'import_type' => 'test', + 'non_existent_attr' => 'test', + 'some_html' => '<p>dodgy html</p>', + 'legit_html' => '<p>legit html</p>', + '_html' => '<p>perfectly ordinary html</p>', + 'cached_markdown_version' => 12345, + 'custom_attributes' => 'test', + 'some_attributes_metadata' => 'test', + 'group_id' => 99, + 'commit_id' => 99, + 'issue_ids' => [1, 2, 3], + 'merge_request_ids' => [1, 2, 3], + 'note_ids' => [1, 2, 3], + 'remote_attachment_url' => 'http://something.dodgy', + 'remote_attachment_request_header' => 'bad value', + 'remote_attachment_urls' => %w(http://something.dodgy http://something.okay), + 'attributes' => { + 'issue_ids' => [1, 2, 3], + 'merge_request_ids' => [1, 2, 3], + 'note_ids' => [1, 2, 3] + }, + 'variables_attributes' => { + 'id' => 1 + }, + 'attr_with_nested_attrs' => { + 'nested_id' => 1, + 'nested_attr' => 2 + } + } + end + + let(:expected_hash) do + { + 'random_id_in_the_middle' => 99, + 'notid' => 99, + 'import_source' => 'test', + 'import_type' => 'test', + 'non_existent_attr' => 'test', + 'attr_with_nested_attrs' => { + 'nested_attr' => 2 + } + } + end + + it 'removes prohibited attributes' do + transformed_hash = subject.transform(nil, hash) + + expect(transformed_hash).to eq(expected_hash) + end + end +end diff --git a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb index 3949dd23b49..c9b481388c3 100644 --- a/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/group_pipeline_spec.rb @@ -72,7 +72,6 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do describe 'pipeline parts' do it { expect(described_class).to include_module(BulkImports::Pipeline) } - it { expect(described_class).to include_module(BulkImports::Pipeline::Attributes) } it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) } it 'has extractors' do @@ -90,13 +89,17 @@ RSpec.describe BulkImports::Groups::Pipelines::GroupPipeline do it 'has transformers' do expect(described_class.transformers) .to contain_exactly( - { klass: BulkImports::Common::Transformers::GraphqlCleanerTransformer, options: nil }, + { klass: BulkImports::Common::Transformers::HashKeyDigger, options: { key_path: %w[data group] } }, { klass: BulkImports::Common::Transformers::UnderscorifyKeysTransformer, options: nil }, - { klass: BulkImports::Groups::Transformers::GroupAttributesTransformer, options: nil }) + { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }, + { klass: BulkImports::Groups::Transformers::GroupAttributesTransformer, options: nil } + ) end it 'has loaders' do - expect(described_class.loaders).to contain_exactly({ klass: BulkImports::Groups::Loaders::GroupLoader, options: nil }) + expect(described_class.loaders).to contain_exactly({ + klass: BulkImports::Groups::Loaders::GroupLoader, options: nil + }) end end end diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb index 60a4a796682..788a6e98c45 100644 --- a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb @@ -55,7 +55,6 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do describe 'pipeline parts' do it { expect(described_class).to include_module(BulkImports::Pipeline) } - it { expect(described_class).to include_module(BulkImports::Pipeline::Attributes) } it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) } it 'has extractors' do @@ -67,8 +66,8 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do it 'has transformers' do expect(described_class.transformers).to contain_exactly( - klass: BulkImports::Groups::Transformers::SubgroupToEntityTransformer, - options: nil + { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }, + { klass: BulkImports::Groups::Transformers::SubgroupToEntityTransformer, options: nil } ) end diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb index 95ac5925c97..95dca7fc486 100644 --- a/spec/lib/bulk_imports/importers/group_importer_spec.rb +++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb @@ -18,12 +18,12 @@ RSpec.describe BulkImports::Importers::GroupImporter do subject { described_class.new(bulk_import_entity) } before do + allow(Gitlab).to receive(:ee?).and_return(false) allow(BulkImports::Pipeline::Context).to receive(:new).and_return(context) - stub_http_requests end describe '#execute' do - it "starts the entity and run its pipelines" do + it 'starts the entity and run its pipelines' do expect(bulk_import_entity).to receive(:start).and_call_original expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context @@ -32,6 +32,18 @@ RSpec.describe BulkImports::Importers::GroupImporter do expect(bulk_import_entity.reload).to be_finished end + + context 'when failed' do + let(:bulk_import_entity) { create(:bulk_import_entity, :failed, bulk_import: bulk_import) } + + it 'does not transition entity to finished state' do + allow(bulk_import_entity).to receive(:start!) + + subject.execute + + expect(bulk_import_entity.reload).to be_failed + end + end end def expect_to_run_pipeline(klass, context:) @@ -39,18 +51,4 @@ RSpec.describe BulkImports::Importers::GroupImporter do expect(pipeline).to receive(:run).with(context) end end - - def stub_http_requests - double_response = double( - code: 200, - success?: true, - parsed_response: {}, - headers: {} - ) - - allow_next_instance_of(BulkImports::Clients::Http) do |client| - allow(client).to receive(:get).and_return(double_response) - allow(client).to receive(:post).and_return(double_response) - end - end end diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb index 8c882c799ec..60833e83dcc 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -3,26 +3,32 @@ require 'spec_helper' RSpec.describe BulkImports::Pipeline::Runner do - describe 'pipeline runner' do - before do - extractor = Class.new do - def initialize(options = {}); end + let(:extractor) do + Class.new do + def initialize(options = {}); end - def extract(context); end - end + def extract(context); end + end + end - transformer = Class.new do - def initialize(options = {}); end + let(:transformer) do + Class.new do + def initialize(options = {}); end - def transform(context, entry); end - end + def transform(context); end + end + end - loader = Class.new do - def initialize(options = {}); end + let(:loader) do + Class.new do + def initialize(options = {}); end - def load(context, entry); end - end + def load(context); end + end + end + describe 'pipeline runner' do + before do stub_const('BulkImports::Extractor', extractor) stub_const('BulkImports::Transformer', transformer) stub_const('BulkImports::Loader', loader) @@ -38,37 +44,126 @@ RSpec.describe BulkImports::Pipeline::Runner do stub_const('BulkImports::MyPipeline', pipeline) end - it 'runs pipeline extractor, transformer, loader' do - context = instance_double( - BulkImports::Pipeline::Context, - entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group') - ) - entries = [{ foo: :bar }] - - expect_next_instance_of(BulkImports::Extractor) do |extractor| - expect(extractor).to receive(:extract).with(context).and_return(entries) + context 'when entity is not marked as failed' do + let(:context) do + instance_double( + BulkImports::Pipeline::Context, + entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group', failed?: false) + ) end - expect_next_instance_of(BulkImports::Transformer) do |transformer| - expect(transformer).to receive(:transform).with(context, entries.first).and_return(entries.first) + it 'runs pipeline extractor, transformer, loader' do + entries = [{ foo: :bar }] + + expect_next_instance_of(BulkImports::Extractor) do |extractor| + expect(extractor).to receive(:extract).with(context).and_return(entries) + end + + expect_next_instance_of(BulkImports::Transformer) do |transformer| + expect(transformer).to receive(:transform).with(context, entries.first).and_return(entries.first) + end + + expect_next_instance_of(BulkImports::Loader) do |loader| + expect(loader).to receive(:load).with(context, entries.first) + end + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:info) + .with( + message: 'Pipeline started', + pipeline_class: 'BulkImports::MyPipeline', + bulk_import_entity_id: 1, + bulk_import_entity_type: 'group' + ) + expect(logger).to receive(:info) + .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', extractor: 'BulkImports::Extractor') + expect(logger).to receive(:info) + .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', transformer: 'BulkImports::Transformer') + expect(logger).to receive(:info) + .with(bulk_import_entity_id: 1, bulk_import_entity_type: 'group', loader: 'BulkImports::Loader') + end + + BulkImports::MyPipeline.new.run(context) end - expect_next_instance_of(BulkImports::Loader) do |loader| - expect(loader).to receive(:load).with(context, entries.first) + context 'when exception is raised' do + let(:entity) { create(:bulk_import_entity, :created) } + let(:context) { BulkImports::Pipeline::Context.new(entity: entity) } + + before do + allow_next_instance_of(BulkImports::Extractor) do |extractor| + allow(extractor).to receive(:extract).with(context).and_raise(StandardError, 'Error!') + end + end + + it 'logs import failure' do + BulkImports::MyPipeline.new.run(context) + + failure = entity.failures.first + + expect(failure).to be_present + expect(failure.pipeline_class).to eq('BulkImports::MyPipeline') + expect(failure.exception_class).to eq('StandardError') + expect(failure.exception_message).to eq('Error!') + end + + context 'when pipeline is marked to abort on failure' do + before do + BulkImports::MyPipeline.abort_on_failure! + end + + it 'marks entity as failed' do + BulkImports::MyPipeline.new.run(context) + + expect(entity.failed?).to eq(true) + end + + it 'logs warn message' do + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:warn) + .with( + message: 'Pipeline failed', + pipeline_class: 'BulkImports::MyPipeline', + bulk_import_entity_id: entity.id, + bulk_import_entity_type: entity.source_type + ) + end + + BulkImports::MyPipeline.new.run(context) + end + end + + context 'when pipeline is not marked to abort on failure' do + it 'marks entity as failed' do + BulkImports::MyPipeline.new.run(context) + + expect(entity.failed?).to eq(false) + end + end end + end - expect_next_instance_of(Gitlab::Import::Logger) do |logger| - expect(logger).to receive(:info) - .with(message: "Pipeline started", pipeline: 'BulkImports::MyPipeline', entity: 1, entity_type: 'group') - expect(logger).to receive(:info) - .with(entity: 1, entity_type: 'group', extractor: 'BulkImports::Extractor') - expect(logger).to receive(:info) - .with(entity: 1, entity_type: 'group', transformer: 'BulkImports::Transformer') - expect(logger).to receive(:info) - .with(entity: 1, entity_type: 'group', loader: 'BulkImports::Loader') + context 'when entity is marked as failed' do + let(:context) do + instance_double( + BulkImports::Pipeline::Context, + entity: instance_double(BulkImports::Entity, id: 1, source_type: 'group', failed?: true) + ) end - BulkImports::MyPipeline.new.run(context) + it 'logs and returns without execution' do + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:info) + .with( + message: 'Skipping due to failed pipeline status', + pipeline_class: 'BulkImports::MyPipeline', + bulk_import_entity_id: 1, + bulk_import_entity_type: 'group' + ) + end + + BulkImports::MyPipeline.new.run(context) + end end end end diff --git a/spec/lib/bulk_imports/pipeline/attributes_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb index 54c5dbd4cae..94052be7df2 100644 --- a/spec/lib/bulk_imports/pipeline/attributes_spec.rb +++ b/spec/lib/bulk_imports/pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BulkImports::Pipeline::Attributes do +RSpec.describe BulkImports::Pipeline do describe 'pipeline attributes' do before do stub_const('BulkImports::Extractor', Class.new) @@ -10,7 +10,9 @@ RSpec.describe BulkImports::Pipeline::Attributes do stub_const('BulkImports::Loader', Class.new) klass = Class.new do - include BulkImports::Pipeline::Attributes + include BulkImports::Pipeline + + abort_on_failure! extractor BulkImports::Extractor, { foo: :bar } transformer BulkImports::Transformer, { foo: :bar } @@ -25,6 +27,7 @@ RSpec.describe BulkImports::Pipeline::Attributes do expect(BulkImports::MyPipeline.extractors).to contain_exactly({ klass: BulkImports::Extractor, options: { foo: :bar } }) expect(BulkImports::MyPipeline.transformers).to contain_exactly({ klass: BulkImports::Transformer, options: { foo: :bar } }) expect(BulkImports::MyPipeline.loaders).to contain_exactly({ klass: BulkImports::Loader, options: { foo: :bar } }) + expect(BulkImports::MyPipeline.abort_on_failure?).to eq(true) end end @@ -36,6 +39,7 @@ RSpec.describe BulkImports::Pipeline::Attributes do BulkImports::MyPipeline.extractor(klass, options) BulkImports::MyPipeline.transformer(klass, options) BulkImports::MyPipeline.loader(klass, options) + BulkImports::MyPipeline.abort_on_failure! expect(BulkImports::MyPipeline.extractors) .to contain_exactly( @@ -51,6 +55,8 @@ RSpec.describe BulkImports::Pipeline::Attributes do .to contain_exactly( { klass: BulkImports::Loader, options: { foo: :bar } }, { klass: klass, options: options }) + + expect(BulkImports::MyPipeline.abort_on_failure?).to eq(true) end end end diff --git a/spec/lib/feature/definition_spec.rb b/spec/lib/feature/definition_spec.rb index fa0207d829a..21120012927 100644 --- a/spec/lib/feature/definition_spec.rb +++ b/spec/lib/feature/definition_spec.rb @@ -64,6 +64,11 @@ RSpec.describe Feature::Definition do expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: false) } .to raise_error(/The `default_enabled:` of `feature_flag` is not equal to config/) end + + it 'allows passing `default_enabled: :yaml`' do + expect { definition.valid_usage!(type_in_code: :development, default_enabled_in_code: :yaml) } + .not_to raise_error + end end end @@ -75,7 +80,7 @@ RSpec.describe Feature::Definition do describe '.load_from_file' do it 'properly loads a definition from file' do - expect(File).to receive(:read).with(path) { yaml_content } + expect_file_read(path, content: yaml_content) expect(described_class.send(:load_from_file, path).attributes) .to eq(definition.attributes) @@ -93,7 +98,7 @@ RSpec.describe Feature::Definition do context 'for invalid definition' do it 'raises exception' do - expect(File).to receive(:read).with(path) { '{}' } + expect_file_read(path, content: '{}') expect do described_class.send(:load_from_file, path) @@ -209,4 +214,58 @@ RSpec.describe Feature::Definition do end end end + + describe '.defaul_enabled?' do + subject { described_class.default_enabled?(key) } + + context 'when feature flag exist' do + let(:key) { definition.key } + + before do + allow(described_class).to receive(:definitions) do + { definition.key => definition } + end + end + + context 'when default_enabled is true' do + it 'returns the value from the definition' do + expect(subject).to eq(true) + end + end + + context 'when default_enabled is false' do + let(:attributes) do + { name: 'feature_flag', + type: 'development', + default_enabled: false } + end + + it 'returns the value from the definition' do + expect(subject).to eq(false) + end + end + end + + context 'when feature flag does not exist' do + let(:key) { :unknown_feature_flag } + + context 'when on dev or test environment' do + it 'raises an error' do + expect { subject }.to raise_error( + Feature::InvalidFeatureFlagError, + "The feature flag YAML definition for 'unknown_feature_flag' does not exist") + end + end + + context 'when on production environment' do + before do + allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + end + + it 'returns false' do + expect(subject).to eq(false) + end + end + end + end end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 5dff9dbd995..1bcb2223012 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -249,10 +249,12 @@ RSpec.describe Feature, stub_feature_flags: false do Feature::Definition.new('development/my_feature_flag.yml', name: 'my_feature_flag', type: 'development', - default_enabled: false + default_enabled: default_enabled ).tap(&:validate!) end + let(:default_enabled) { false } + before do stub_env('LAZILY_CREATE_FEATURE_FLAG', '0') @@ -275,6 +277,63 @@ RSpec.describe Feature, stub_feature_flags: false do expect { described_class.enabled?(:my_feature_flag, default_enabled: true) } .to raise_error(/The `default_enabled:` of/) end + + context 'when `default_enabled: :yaml` is used in code' do + it 'reads the default from the YAML definition' do + expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(false) + end + + context 'when default_enabled is true in the YAML definition' do + let(:default_enabled) { true } + + it 'reads the default from the YAML definition' do + expect(described_class.enabled?(:my_feature_flag, default_enabled: :yaml)).to eq(true) + end + end + + context 'when YAML definition does not exist for an optional type' do + let(:optional_type) { described_class::Shared::TYPES.find { |name, attrs| attrs[:optional] }.first } + + context 'when in dev or test environment' do + it 'raises an error for dev' do + expect { described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml) } + .to raise_error( + Feature::InvalidFeatureFlagError, + "The feature flag YAML definition for 'non_existent_flag' does not exist") + end + end + + context 'when in production' do + before do + allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + end + + context 'when database exists' do + before do + allow(Gitlab::Database).to receive(:exists?).and_return(true) + end + + it 'checks the persisted status and returns false' do + expect(described_class).to receive(:get).with(:non_existent_flag).and_call_original + + expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false) + end + end + + context 'when database does not exist' do + before do + allow(Gitlab::Database).to receive(:exists?).and_return(false) + end + + it 'returns false without checking the status in the database' do + expect(described_class).not_to receive(:get) + + expect(described_class.enabled?(:non_existent_flag, type: optional_type, default_enabled: :yaml)).to eq(false) + end + end + end + end + end end end @@ -300,7 +359,119 @@ RSpec.describe Feature, stub_feature_flags: false do end end + shared_examples_for 'logging' do + let(:expected_action) { } + let(:expected_extra) { } + + it 'logs the event' do + expect(Feature.logger).to receive(:info).with(key: key, action: expected_action, **expected_extra) + + subject + end + end + + describe '.enable' do + subject { described_class.enable(key, thing) } + + let(:key) { :awesome_feature } + let(:thing) { true } + + it_behaves_like 'logging' do + let(:expected_action) { :enable } + let(:expected_extra) { { "extra.thing" => "true" } } + end + + context 'when thing is an actor' do + let(:thing) { create(:project) } + + it_behaves_like 'logging' do + let(:expected_action) { :enable } + let(:expected_extra) { { "extra.thing" => "#{thing.flipper_id}" } } + end + end + end + + describe '.disable' do + subject { described_class.disable(key, thing) } + + let(:key) { :awesome_feature } + let(:thing) { false } + + it_behaves_like 'logging' do + let(:expected_action) { :disable } + let(:expected_extra) { { "extra.thing" => "false" } } + end + + context 'when thing is an actor' do + let(:thing) { create(:project) } + + it_behaves_like 'logging' do + let(:expected_action) { :disable } + let(:expected_extra) { { "extra.thing" => "#{thing.flipper_id}" } } + end + end + end + + describe '.enable_percentage_of_time' do + subject { described_class.enable_percentage_of_time(key, percentage) } + + let(:key) { :awesome_feature } + let(:percentage) { 50 } + + it_behaves_like 'logging' do + let(:expected_action) { :enable_percentage_of_time } + let(:expected_extra) { { "extra.percentage" => "#{percentage}" } } + end + end + + describe '.disable_percentage_of_time' do + subject { described_class.disable_percentage_of_time(key) } + + let(:key) { :awesome_feature } + + it_behaves_like 'logging' do + let(:expected_action) { :disable_percentage_of_time } + let(:expected_extra) { {} } + end + end + + describe '.enable_percentage_of_actors' do + subject { described_class.enable_percentage_of_actors(key, percentage) } + + let(:key) { :awesome_feature } + let(:percentage) { 50 } + + it_behaves_like 'logging' do + let(:expected_action) { :enable_percentage_of_actors } + let(:expected_extra) { { "extra.percentage" => "#{percentage}" } } + end + end + + describe '.disable_percentage_of_actors' do + subject { described_class.disable_percentage_of_actors(key) } + + let(:key) { :awesome_feature } + + it_behaves_like 'logging' do + let(:expected_action) { :disable_percentage_of_actors } + let(:expected_extra) { {} } + end + end + describe '.remove' do + subject { described_class.remove(key) } + + let(:key) { :awesome_feature } + + before do + described_class.enable(key) + end + + it_behaves_like 'logging' do + let(:expected_action) { :remove } + let(:expected_extra) { {} } + end + context 'for a non-persisted feature' do it 'returns nil' do expect(described_class.remove(:non_persisted_feature_flag)).to be_nil diff --git a/spec/lib/gitlab/anonymous_session_spec.rb b/spec/lib/gitlab/anonymous_session_spec.rb index 671d452ad13..245ca02e91a 100644 --- a/spec/lib/gitlab/anonymous_session_spec.rb +++ b/spec/lib/gitlab/anonymous_session_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Gitlab::AnonymousSession, :clean_gitlab_redis_shared_state do end it 'adds expiration time to key' do - Timecop.freeze do + freeze_time do subject.count_session_ip Gitlab::Redis::SharedState.with do |redis| diff --git a/spec/lib/gitlab/asciidoc/html5_converter_spec.rb b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb new file mode 100644 index 00000000000..84c2cda496e --- /dev/null +++ b/spec/lib/gitlab/asciidoc/html5_converter_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Asciidoc::Html5Converter do + describe 'convert AsciiDoc to HTML5' do + it 'appends user-content- prefix on ref (anchor)' do + doc = Asciidoctor::Document.new('') + anchor = Asciidoctor::Inline.new(doc, :anchor, '', type: :ref, id: 'cross-references') + converter = Gitlab::Asciidoc::Html5Converter.new('gitlab_html5') + html = converter.convert_inline_anchor(anchor) + expect(html).to eq('<a id="user-content-cross-references"></a>') + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 6b93634690c..36e4decdead 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -20,7 +20,7 @@ module Gitlab expected_asciidoc_opts = { safe: :secure, backend: :gitlab_html5, - attributes: described_class::DEFAULT_ADOC_ATTRS, + attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }), extensions: be_a(Proc) } @@ -35,7 +35,7 @@ module Gitlab expected_asciidoc_opts = { safe: :secure, backend: :gitlab_html5, - attributes: described_class::DEFAULT_ADOC_ATTRS, + attributes: described_class::DEFAULT_ADOC_ATTRS.merge({ "kroki-server-url" => nil }), extensions: be_a(Proc) } @@ -252,6 +252,27 @@ module Gitlab end end + context 'with xrefs' do + it 'preserves ids' do + input = <<~ADOC + Learn how to xref:cross-references[use cross references]. + + [[cross-references]]A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref). + ADOC + + output = <<~HTML + <div> + <p>Learn how to <a href="#cross-references">use cross references</a>.</p> + </div> + <div> + <p><a id="user-content-cross-references"></a>A link to another location within an AsciiDoc document or between AsciiDoc documents is called a cross reference (also referred to as an xref).</p> + </div> + HTML + + expect(render(input, context)).to include(output.strip) + end + end + context 'with checklist' do it 'preserves classes' do input = <<~ADOC @@ -462,6 +483,34 @@ module Gitlab expect(render(input, context)).to include(output.strip) end end + + context 'with Kroki enabled' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:kroki_enabled).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:kroki_url).and_return('https://kroki.io') + end + + it 'converts a graphviz diagram to image' do + input = <<~ADOC + [graphviz] + .... + digraph G { + Hello->World + } + .... + ADOC + + output = <<~HTML + <div> + <div> + <a class="no-attachment-icon" href="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka" target="_blank" rel="noopener noreferrer"><img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt="Diagram" class="lazy" data-src="https://kroki.io/graphviz/svg/eNpLyUwvSizIUHBXqOZSUPBIzcnJ17ULzy_KSeGqBQCEzQka"></a> + </div> + </div> + HTML + + expect(render(input, context)).to include(output.strip) + end + end end context 'with project' do diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb index 3c19ef0bd1b..f927d5912bb 100644 --- a/spec/lib/gitlab/auth/auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/auth_finders_spec.rb @@ -147,6 +147,13 @@ RSpec.describe Gitlab::Auth::AuthFinders do expect(find_user_from_feed_token(:rss)).to eq user end + it 'returns nil if valid feed_token and disabled' do + allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true) + set_param(:feed_token, user.feed_token) + + expect(find_user_from_feed_token(:rss)).to be_nil + end + it 'returns nil if feed_token is blank' do expect(find_user_from_feed_token(:rss)).to be_nil end @@ -377,6 +384,16 @@ RSpec.describe Gitlab::Auth::AuthFinders do expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError) end + + context 'when using a non-prefixed access token' do + let(:personal_access_token) { create(:personal_access_token, :no_prefix, user: user) } + + it 'returns user' do + set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}") + + expect(find_user_from_access_token).to eq user + end + end end end diff --git a/spec/lib/gitlab/auth/crowd/authentication_spec.rb b/spec/lib/gitlab/auth/crowd/authentication_spec.rb new file mode 100644 index 00000000000..71eb8036fdd --- /dev/null +++ b/spec/lib/gitlab/auth/crowd/authentication_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Crowd::Authentication do + let(:provider) { 'crowd' } + let(:login) { generate(:username) } + let(:password) { 'password' } + let(:crowd_auth) { described_class.new(provider) } + let(:user_info) { { user: login } } + + describe 'login' do + before do + allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).with(provider).and_return(true) + allow(crowd_auth).to receive(:user_info_from_authentication).and_return(user_info) + end + + it "finds the user if authentication is successful" do + create(:omniauth_user, extern_uid: login, username: login, provider: provider) + + expect(crowd_auth.login(login, password)).to be_truthy + end + + it "is false if the user does not exist" do + expect(crowd_auth.login(login, password)).to be_falsey + end + + it "is false if the authentication fails" do + allow(crowd_auth).to receive(:user_info_from_authentication).and_return(nil) + + expect(crowd_auth.login(login, password)).to be_falsey + end + + it "fails when crowd is disabled" do + allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).with('crowd').and_return(false) + + expect(crowd_auth.login(login, password)).to be_falsey + end + + it "fails if no login is supplied" do + expect(crowd_auth.login('', password)).to be_falsey + end + + it "fails if no password is supplied" do + expect(crowd_auth.login(login, '')).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb index ccaed94b5c8..e910ac09448 100644 --- a/spec/lib/gitlab/auth/ldap/user_spec.rb +++ b/spec/lib/gitlab/auth/ldap/user_spec.rb @@ -49,23 +49,6 @@ RSpec.describe Gitlab::Auth::Ldap::User do end end - describe '.find_by_uid_and_provider' do - let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' } - - it 'retrieves the correct user' do - special_info = { - name: 'John Åström', - email: 'john@example.com', - nickname: 'jastrom' - } - special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info) - special_chars_user = described_class.new(special_hash) - user = special_chars_user.save - - expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user - end - end - describe 'find or create' do it "finds the user if already existing" do create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain') diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index 243d0a4cb45..6c6cee9c273 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -25,6 +25,23 @@ RSpec.describe Gitlab::Auth::OAuth::User do let(:ldap_user) { Gitlab::Auth::Ldap::Person.new(Net::LDAP::Entry.new, 'ldapmain') } + describe '.find_by_uid_and_provider' do + let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' } + + it 'retrieves the correct user' do + special_info = { + name: 'John Åström', + email: 'john@example.com', + nickname: 'jastrom' + } + special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info) + special_chars_user = described_class.new(special_hash) + user = special_chars_user.save + + expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user + end + end + describe '#persisted?' do let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } diff --git a/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb b/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb new file mode 100644 index 00000000000..928aade4008 --- /dev/null +++ b/spec/lib/gitlab/auth/otp/session_enforcer_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Otp::SessionEnforcer, :clean_gitlab_redis_shared_state do + let_it_be(:key) { create(:key)} + + describe '#update_session' do + it 'registers a session in Redis' do + redis = double(:redis) + expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) + + expect(redis).to( + receive(:setex) + .with("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", + described_class::DEFAULT_EXPIRATION, + true) + .once) + + described_class.new(key).update_session + end + end + + describe '#access_restricted?' do + subject { described_class.new(key).access_restricted? } + + context 'with existing session' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.set("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", true ) + end + end + + it { is_expected.to be_falsey } + end + + context 'without an existing session' do + it { is_expected.to be_truthy } + end + end +end diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb index 18fd6d08057..88a245b6b10 100644 --- a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb @@ -12,30 +12,32 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do let(:api_token) { 's3cr3t' } let(:forti_authenticator_auth_url) { "https://#{host}:#{port}/api/v1/auth/" } + let(:response_status) { 200 } subject(:validate) { described_class.new(user).validate(otp_code) } before do - stub_feature_flags(forti_authenticator: true) + stub_feature_flags(forti_authenticator: user) stub_forti_authenticator_config( + enabled: true, host: host, port: port, username: api_username, - token: api_token + access_token: api_token ) request_body = { username: user.username, token_code: otp_code } stub_request(:post, forti_authenticator_auth_url) - .with(body: JSON(request_body), headers: { 'Content-Type' => 'application/json' }) - .to_return(status: response_status, body: '', headers: {}) + .with(body: JSON(request_body), + headers: { 'Content-Type': 'application/json' }, + basic_auth: [api_username, api_token]) + .to_return(status: response_status, body: '') end context 'successful validation' do - let(:response_status) { 200 } - it 'returns success' do expect(validate[:status]).to eq(:success) end @@ -49,6 +51,16 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do end end + context 'unexpected error' do + it 'returns error' do + error_message = 'boom!' + stub_request(:post, forti_authenticator_auth_url).to_raise(StandardError.new(error_message)) + + expect(validate[:status]).to eq(:error) + expect(validate[:message]).to eq(error_message) + end + end + def stub_forti_authenticator_config(forti_authenticator_settings) allow(::Gitlab.config.forti_authenticator).to(receive_messages(forti_authenticator_settings)) end diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb new file mode 100644 index 00000000000..1580fc82279 --- /dev/null +++ b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do + let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } + + let(:url) { 'https://ftc.example.com:9696/api/v1' } + let(:client_id) { 'client_id' } + let(:client_secret) { 's3cr3t' } + let(:access_token_create_url) { url + '/login' } + let(:otp_verification_url) { url + '/auth' } + let(:access_token) { 'an_access_token' } + let(:access_token_create_response_body) { '' } + + subject(:validate) { described_class.new(user).validate(otp_code) } + + before do + stub_feature_flags(forti_token_cloud: user) + + stub_const("#{described_class}::BASE_API_URL", url) + + stub_forti_token_cloud_config( + enabled: true, + client_id: client_id, + client_secret: client_secret + ) + + access_token_request_body = { client_id: client_id, + client_secret: client_secret } + + stub_request(:post, access_token_create_url) + .with(body: JSON(access_token_request_body), headers: { 'Content-Type' => 'application/json' }) + .to_return( + status: access_token_create_response_status, + body: Gitlab::Json.generate(access_token_create_response_body), + headers: {} + ) + end + + context 'access token is created successfully' do + let(:access_token_create_response_body) { { access_token: access_token, expires_in: 3600 } } + let(:access_token_create_response_status) { 201 } + + before do + otp_verification_request_body = { username: user.username, + token: otp_code } + + stub_request(:post, otp_verification_url) + .with(body: JSON(otp_verification_request_body), + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{access_token}" + }) + .to_return(status: otp_verification_response_status, body: '', headers: {}) + end + + context 'otp verification is successful' do + let(:otp_verification_response_status) { 200 } + + it 'returns success' do + expect(validate[:status]).to eq(:success) + end + end + + context 'otp verification is not successful' do + let(:otp_verification_response_status) { 401 } + + it 'returns error' do + expect(validate[:status]).to eq(:error) + end + end + end + + context 'access token creation fails' do + let(:access_token_create_response_status) { 400 } + + it 'returns error' do + expect(validate[:status]).to eq(:error) + end + end + + def stub_forti_token_cloud_config(forti_token_cloud_settings) + allow(::Gitlab.config.forti_token_cloud).to(receive_messages(forti_token_cloud_settings)) + end +end diff --git a/spec/lib/gitlab/auth/request_authenticator_spec.rb b/spec/lib/gitlab/auth/request_authenticator_spec.rb index b89ceb37076..ef6b1d72712 100644 --- a/spec/lib/gitlab/auth/request_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/request_authenticator_spec.rb @@ -50,13 +50,13 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_return(access_token_user) allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) - expect(subject.find_sessionless_user([:api])).to eq access_token_user + expect(subject.find_sessionless_user(:api)).to eq access_token_user end it 'returns feed_token user if no access_token user found' do allow_any_instance_of(described_class).to receive(:find_user_from_feed_token).and_return(feed_token_user) - expect(subject.find_sessionless_user([:api])).to eq feed_token_user + expect(subject.find_sessionless_user(:api)).to eq feed_token_user end it 'returns static_object_token user if no feed_token user found' do @@ -64,7 +64,7 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do .to receive(:find_user_from_static_object_token) .and_return(static_object_token_user) - expect(subject.find_sessionless_user([:api])).to eq static_object_token_user + expect(subject.find_sessionless_user(:api)).to eq static_object_token_user end it 'returns job_token user if no static_object_token user found' do @@ -72,17 +72,61 @@ RSpec.describe Gitlab::Auth::RequestAuthenticator do .to receive(:find_user_from_job_token) .and_return(job_token_user) - expect(subject.find_sessionless_user([:api])).to eq job_token_user + expect(subject.find_sessionless_user(:api)).to eq job_token_user end it 'returns nil if no user found' do - expect(subject.find_sessionless_user([:api])).to be_blank + expect(subject.find_sessionless_user(:api)).to be_blank end it 'rescue Gitlab::Auth::AuthenticationError exceptions' do allow_any_instance_of(described_class).to receive(:find_user_from_web_access_token).and_raise(Gitlab::Auth::UnauthorizedError) - expect(subject.find_sessionless_user([:api])).to be_blank + expect(subject.find_sessionless_user(:api)).to be_blank + end + end + + describe '#find_personal_access_token_from_http_basic_auth' do + let_it_be(:personal_access_token) { create(:personal_access_token) } + let_it_be(:user) { personal_access_token.user } + + before do + allow(subject).to receive(:has_basic_credentials?).and_return(true) + allow(subject).to receive(:user_name_and_password).and_return([user.username, personal_access_token.token]) + end + + context 'with API requests' do + before do + env['SCRIPT_NAME'] = '/api/endpoint' + end + + it 'tries to find the user' do + expect(subject.user([:api])).to eq user + end + + it 'returns nil if the token is revoked' do + personal_access_token.revoke! + + expect(subject.user([:api])).to be_blank + end + + it 'returns nil if the token does not have API scope' do + personal_access_token.update!(scopes: ['read_registry']) + + expect(subject.user([:api])).to be_blank + end + end + + context 'without API requests' do + before do + env['SCRIPT_NAME'] = '/web/endpoint' + end + + it 'does not search for job users' do + expect(PersonalAccessToken).not_to receive(:find_by_token) + + expect(subject.user([:api])).to be_nil + end end end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 1768ab41a71..dfd21983682 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -364,20 +364,33 @@ RSpec.describe Gitlab::Auth, :use_clean_rails_memory_store_caching do let_it_be(:project_access_token) { create(:personal_access_token, user: project_bot_user) } context 'with valid project access token' do - before_all do + before do project.add_maintainer(project_bot_user) end - it 'succeeds' do + it 'successfully authenticates the project bot' do expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip')) .to eq(Gitlab::Auth::Result.new(project_bot_user, nil, :personal_access_token, described_class.full_authentication_abilities)) end end context 'with invalid project access token' do - it 'fails' do - expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip')) - .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil)) + context 'when project bot is not a project member' do + it 'fails for a non-project member' do + expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil)) + end + end + + context 'when project bot user is blocked' do + before do + project_bot_user.block! + end + + it 'fails for a blocked project bot' do + expect(gl_auth.find_for_git_client(project_bot_user.username, project_access_token.token, project: project, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(nil, nil, nil, nil)) + end end end end diff --git a/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb b/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb index 2be9c03e5bd..54c14e7a4b8 100644 --- a/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_deployment_clusters_from_deployments_spec.rb @@ -9,10 +9,10 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDeploymentClustersFromDeploy it 'backfills deployment_cluster for all deployments in the given range with a non-null cluster_id' do deployment_clusters = table(:deployment_clusters) - namespace = table(:namespaces).create(name: 'the-namespace', path: 'the-path') - project = table(:projects).create(name: 'the-project', namespace_id: namespace.id) - environment = table(:environments).create(name: 'the-environment', project_id: project.id, slug: 'slug') - cluster = table(:clusters).create(name: 'the-cluster') + namespace = table(:namespaces).create!(name: 'the-namespace', path: 'the-path') + project = table(:projects).create!(name: 'the-project', namespace_id: namespace.id) + environment = table(:environments).create!(name: 'the-environment', project_id: project.id, slug: 'slug') + cluster = table(:clusters).create!(name: 'the-cluster') deployment_data = { cluster_id: cluster.id, project_id: project.id, environment_id: environment.id, ref: 'abc', tag: false, sha: 'sha', status: 1 } expected_deployment_1 = create_deployment(**deployment_data) @@ -21,7 +21,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDeploymentClustersFromDeploy out_of_range_deployment = create_deployment(**deployment_data, cluster_id: cluster.id) # expected to be out of range # to test "ON CONFLICT DO NOTHING" - existing_record_for_deployment_2 = deployment_clusters.create( + existing_record_for_deployment_2 = deployment_clusters.create!( deployment_id: expected_deployment_2.id, cluster_id: expected_deployment_2.cluster_id, kubernetes_namespace: 'production' @@ -38,7 +38,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillDeploymentClustersFromDeploy def create_deployment(**data) @iid ||= 0 @iid += 1 - table(:deployments).create(iid: @iid, **data) + table(:deployments).create!(iid: @iid, **data) end end end diff --git a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb index 8a8edc1af29..539dff86168 100644 --- a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb @@ -81,7 +81,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillProjectRepositories do end it 'returns the correct disk_path using the route entry' do - project_legacy_storage_5.route.update(path: 'zoo/new-test') + project_legacy_storage_5.route.update!(path: 'zoo/new-test') project = described_class.find(project_legacy_storage_5.id) expect(project.disk_path).to eq('zoo/new-test') @@ -93,8 +93,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillProjectRepositories do subgroup.update_column(:parent_id, non_existing_record_id) project = described_class.find(project_orphaned_namespace.id) - project.route.destroy - subgroup.route.destroy + project.route.destroy! + subgroup.route.destroy! expect { project.reload.disk_path } .to raise_error(Gitlab::BackgroundMigration::BackfillProjectRepositories::OrphanedNamespaceError) diff --git a/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb index 4e7a3a33f7e..48c5674822a 100644 --- a/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_project_settings_spec.rb @@ -5,16 +5,16 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::BackfillProjectSettings, schema: 20200114113341 do let(:projects) { table(:projects) } let(:project_settings) { table(:project_settings) } - let(:namespace) { table(:namespaces).create(name: 'user', path: 'user') } - let(:project) { projects.create(namespace_id: namespace.id) } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:project) { projects.create!(namespace_id: namespace.id) } subject { described_class.new } describe '#perform' do it 'creates settings for all projects in range' do - projects.create(id: 5, namespace_id: namespace.id) - projects.create(id: 7, namespace_id: namespace.id) - projects.create(id: 8, namespace_id: namespace.id) + projects.create!(id: 5, namespace_id: namespace.id) + projects.create!(id: 7, namespace_id: namespace.id) + projects.create!(id: 8, namespace_id: namespace.id) subject.perform(5, 7) diff --git a/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb b/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb index 39b49d008d4..9ce6a3227b5 100644 --- a/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_push_rules_id_in_projects_spec.rb @@ -6,21 +6,21 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPushRulesIdInProjects, :migr let(:push_rules) { table(:push_rules) } let(:projects) { table(:projects) } let(:project_settings) { table(:project_settings) } - let(:namespace) { table(:namespaces).create(name: 'user', path: 'user') } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } subject { described_class.new } describe '#perform' do it 'creates new project push_rules for all push rules in the range' do - project_1 = projects.create(id: 1, namespace_id: namespace.id) - project_2 = projects.create(id: 2, namespace_id: namespace.id) - project_3 = projects.create(id: 3, namespace_id: namespace.id) - project_settings_1 = project_settings.create(project_id: project_1.id) - project_settings_2 = project_settings.create(project_id: project_2.id) - project_settings_3 = project_settings.create(project_id: project_3.id) - push_rule_1 = push_rules.create(id: 5, is_sample: false, project_id: project_1.id) - push_rule_2 = push_rules.create(id: 6, is_sample: false, project_id: project_2.id) - push_rules.create(id: 8, is_sample: false, project_id: 3) + project_1 = projects.create!(id: 1, namespace_id: namespace.id) + project_2 = projects.create!(id: 2, namespace_id: namespace.id) + project_3 = projects.create!(id: 3, namespace_id: namespace.id) + project_settings_1 = project_settings.create!(project_id: project_1.id) + project_settings_2 = project_settings.create!(project_id: project_2.id) + project_settings_3 = project_settings.create!(project_id: project_3.id) + push_rule_1 = push_rules.create!(id: 5, is_sample: false, project_id: project_1.id) + push_rule_2 = push_rules.create!(id: 6, is_sample: false, project_id: project_2.id) + push_rules.create!(id: 8, is_sample: false, project_id: 3) subject.perform(5, 7) diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index a23b74bcaca..50e799908c6 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat let(:user_name) { 'Test' } let!(:user) do - users.create(id: 1, + users.create!(id: 1, email: 'user@example.com', projects_limit: 10, username: 'test', @@ -25,7 +25,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat end let!(:migration_bot) do - users.create(id: 100, + users.create!(id: 100, email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}", user_type: HasUserType::USER_TYPES[:migration_bot], name: 'GitLab Migration Bot', @@ -33,9 +33,9 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat username: 'bot') end - let!(:snippet_with_repo) { snippets.create(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } - let!(:snippet_with_empty_repo) { snippets.create(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } - let!(:snippet_without_repo) { snippets.create(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } + let!(:snippet_with_repo) { snippets.create!(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } + let!(:snippet_with_empty_repo) { snippets.create!(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } + let!(:snippet_without_repo) { snippets.create!(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } let(:file_name) { 'file_name.rb' } let(:content) { 'content' } @@ -197,8 +197,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat end with_them do - let!(:snippet_with_invalid_path) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: invalid_file_name, content: content) } - let!(:snippet_with_valid_path) { snippets.create(id: 5, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } + let!(:snippet_with_invalid_path) { snippets.create!(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: invalid_file_name, content: content) } + let!(:snippet_with_valid_path) { snippets.create!(id: 5, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } let(:ids) { [4, 5] } after do @@ -241,7 +241,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat context 'when user name is invalid' do let(:user_name) { '.' } - let!(:snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } + let!(:snippet) { snippets.create!(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) } let(:ids) { [4, 4] } after do @@ -254,7 +254,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat context 'when both user name and snippet file_name are invalid' do let(:user_name) { '.' } let!(:other_user) do - users.create(id: 2, + users.create!(id: 2, email: 'user2@example.com', projects_limit: 10, username: 'test2', @@ -265,8 +265,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat confirmed_at: 1.day.ago) end - let!(:invalid_snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) } - let!(:snippet) { snippets.create(id: 5, type: 'PersonalSnippet', author_id: other_user.id, file_name: file_name, content: content) } + let!(:invalid_snippet) { snippets.create!(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) } + let!(:snippet) { snippets.create!(id: 5, type: 'PersonalSnippet', author_id: other_user.id, file_name: file_name, content: content) } let(:ids) { [4, 5] } after do diff --git a/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb b/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb index e2175c41513..d503824041b 100644 --- a/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb +++ b/spec/lib/gitlab/background_migration/fix_projects_without_project_feature_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutProjectFeature, sc let(:projects) { table(:projects) } let(:project_features) { table(:project_features) } - let(:namespace) { namespaces.create(name: 'foo', path: 'foo') } + let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') } let!(:project) { projects.create!(namespace_id: namespace.id) } let(:private_project_without_feature) { projects.create!(namespace_id: namespace.id, visibility_level: 0) } @@ -15,7 +15,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutProjectFeature, sc let!(:projects_without_feature) { [private_project_without_feature, public_project_without_feature] } before do - project_features.create({ project_id: project.id, pages_access_level: 20 }) + project_features.create!({ project_id: project.id, pages_access_level: 20 }) end subject { described_class.new.perform(Project.minimum(:id), Project.maximum(:id)) } diff --git a/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb b/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb index fe2b206ea74..9a497a9e01a 100644 --- a/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb +++ b/spec/lib/gitlab/background_migration/fix_projects_without_prometheus_service_spec.rb @@ -33,8 +33,8 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, let(:clusters) { table(:clusters) } let(:cluster_groups) { table(:cluster_groups) } let(:clusters_applications_prometheus) { table(:clusters_applications_prometheus) } - let(:namespace) { namespaces.create(name: 'user', path: 'user') } - let(:project) { projects.create(namespace_id: namespace.id) } + let(:namespace) { namespaces.create!(name: 'user', path: 'user') } + let(:project) { projects.create!(namespace_id: namespace.id) } let(:application_statuses) do { @@ -71,7 +71,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'non prometheus services' do it 'does not change them' do other_type = 'SomeOtherService' - services.create(service_params_for(project.id, active: true, type: other_type)) + services.create!(service_params_for(project.id, active: true, type: other_type)) expect { subject.perform(project.id, project.id + 1) }.not_to change { services.where(type: other_type).order(:id).map { |row| row.attributes } } end @@ -85,7 +85,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'template is present for prometheus services' do it 'creates missing services entries', :aggregate_failures do - services.create(service_params_for(nil, template: true, properties: { 'from_template' => true }.to_json)) + services.create!(service_params_for(nil, template: true, properties: { 'from_template' => true }.to_json)) expect { subject.perform(project.id, project.id + 1) }.to change { services.count }.by(1) updated_rows = services.where(template: false).order(:id).map { |row| row.attributes.slice(*columns).symbolize_keys } @@ -97,7 +97,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'prometheus integration services exist' do context 'in active state' do it 'does not change them' do - services.create(service_params_for(project.id, active: true)) + services.create!(service_params_for(project.id, active: true)) expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } } end @@ -105,7 +105,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'not in active state' do it 'sets active attribute to true' do - service = services.create(service_params_for(project.id, active: false)) + service = services.create!(service_params_for(project.id, active: false)) expect { subject.perform(project.id, project.id + 1) }.to change { service.reload.active? }.from(false).to(true) end @@ -113,7 +113,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'prometheus services are configured manually ' do it 'does not change them' do properties = '{"api_url":"http://test.dev","manual_configuration":"1"}' - services.create(service_params_for(project.id, properties: properties, active: false)) + services.create!(service_params_for(project.id, properties: properties, active: false)) expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } } end @@ -123,11 +123,11 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, end context 'k8s cluster shared on instance level' do - let(:cluster) { clusters.create(name: 'cluster', cluster_type: cluster_types[:instance_type]) } + let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:instance_type]) } context 'with installed prometheus application' do before do - clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:installed], version: '123') + clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123') end it_behaves_like 'fix services entries state' @@ -135,7 +135,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'with updated prometheus application' do before do - clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:updated], version: '123') + clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:updated], version: '123') end it_behaves_like 'fix services entries state' @@ -143,7 +143,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'with errored prometheus application' do before do - clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:errored], version: '123') + clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:errored], version: '123') end it 'does not change services entries' do @@ -153,26 +153,26 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, end context 'k8s cluster shared on group level' do - let(:cluster) { clusters.create(name: 'cluster', cluster_type: cluster_types[:group_type]) } + let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:group_type]) } before do - cluster_groups.create(cluster_id: cluster.id, group_id: project.namespace_id) + cluster_groups.create!(cluster_id: cluster.id, group_id: project.namespace_id) end context 'with installed prometheus application' do before do - clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:installed], version: '123') + clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123') end it_behaves_like 'fix services entries state' context 'second k8s cluster without application available' do - let(:namespace_2) { namespaces.create(name: 'namespace2', path: 'namespace2') } - let(:project_2) { projects.create(namespace_id: namespace_2.id) } + let(:namespace_2) { namespaces.create!(name: 'namespace2', path: 'namespace2') } + let(:project_2) { projects.create!(namespace_id: namespace_2.id) } before do - cluster_2 = clusters.create(name: 'cluster2', cluster_type: cluster_types[:group_type]) - cluster_groups.create(cluster_id: cluster_2.id, group_id: project_2.namespace_id) + cluster_2 = clusters.create!(name: 'cluster2', cluster_type: cluster_types[:group_type]) + cluster_groups.create!(cluster_id: cluster_2.id, group_id: project_2.namespace_id) end it 'changed only affected services entries' do @@ -184,7 +184,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'with updated prometheus application' do before do - clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:updated], version: '123') + clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:updated], version: '123') end it_behaves_like 'fix services entries state' @@ -192,7 +192,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'with errored prometheus application' do before do - clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:errored], version: '123') + clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:errored], version: '123') end it 'does not change services entries' do @@ -207,7 +207,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, context 'with inactive service' do it 'does not change services entries' do - services.create(service_params_for(project.id)) + services.create!(service_params_for(project.id)) expect { subject.perform(project.id, project.id + 1) }.not_to change { services.order(:id).map { |row| row.attributes } } end @@ -216,13 +216,13 @@ RSpec.describe Gitlab::BackgroundMigration::FixProjectsWithoutPrometheusService, end context 'k8s cluster for single project' do - let(:cluster) { clusters.create(name: 'cluster', cluster_type: cluster_types[:project_type]) } + let(:cluster) { clusters.create!(name: 'cluster', cluster_type: cluster_types[:project_type]) } let(:cluster_projects) { table(:cluster_projects) } context 'with installed prometheus application' do before do - cluster_projects.create(cluster_id: cluster.id, project_id: project.id) - clusters_applications_prometheus.create(cluster_id: cluster.id, status: application_statuses[:installed], version: '123') + cluster_projects.create!(cluster_id: cluster.id, project_id: project.id) + clusters_applications_prometheus.create!(cluster_id: cluster.id, status: application_statuses[:installed], version: '123') end it 'does not change services entries' do diff --git a/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb b/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb index 7768411828c..0d0ad2cc39e 100644 --- a/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb +++ b/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb @@ -5,18 +5,18 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190620112608 do let(:namespaces) { table(:namespaces) } let(:users) { table(:users) } - let(:user) { users.create(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') } + let(:user) { users.create!(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') } context 'updating the namespace names' do it 'updates a user namespace within range' do - user2 = users.create(name: "Other user's full name", projects_limit: 10, username: 'also-not-null', email: '2') - user_namespace1 = namespaces.create( + user2 = users.create!(name: "Other user's full name", projects_limit: 10, username: 'also-not-null', email: '2') + user_namespace1 = namespaces.create!( id: 2, owner_id: user.id, name: "Should be the user's name", path: user.username ) - user_namespace2 = namespaces.create( + user_namespace2 = namespaces.create!( id: 3, owner_id: user2.id, name: "Should also be the user's name", @@ -30,7 +30,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190 end it 'does not update namespaces out of range' do - user_namespace = namespaces.create( + user_namespace = namespaces.create!( id: 6, owner_id: user.id, name: "Should be the user's name", @@ -42,7 +42,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190 end it 'does not update groups owned by the users' do - user_group = namespaces.create( + user_group = namespaces.create!( id: 2, owner_id: user.id, name: 'A group name', @@ -58,7 +58,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190 context 'namespace route names' do let(:routes) { table(:routes) } let(:namespace) do - namespaces.create( + namespaces.create!( id: 2, owner_id: user.id, name: "Will be updated to the user's name", @@ -67,7 +67,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190 end it "updates the route name if it didn't match the namespace" do - route = routes.create(path: namespace.path, name: 'Incorrect name', source_type: 'Namespace', source_id: namespace.id) + route = routes.create!(path: namespace.path, name: 'Incorrect name', source_type: 'Namespace', source_id: namespace.id) described_class.new.perform(1, 5) @@ -75,7 +75,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190 end it 'updates the route name if it was nil match the namespace' do - route = routes.create(path: namespace.path, name: nil, source_type: 'Namespace', source_id: namespace.id) + route = routes.create!(path: namespace.path, name: nil, source_type: 'Namespace', source_id: namespace.id) described_class.new.perform(1, 5) @@ -83,14 +83,14 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserNamespaceNames, schema: 20190 end it "doesn't update group routes" do - route = routes.create(path: 'group-path', name: 'Group name', source_type: 'Group', source_id: namespace.id) + route = routes.create!(path: 'group-path', name: 'Group name', source_type: 'Group', source_id: namespace.id) expect { described_class.new.perform(1, 5) } .not_to change { route.reload.name } end it "doesn't touch routes for namespaces out of range" do - user_namespace = namespaces.create( + user_namespace = namespaces.create!( id: 6, owner_id: user.id, name: "Should be the user's name", diff --git a/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb b/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb index 4c04043ebd0..211693d917b 100644 --- a/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb +++ b/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb @@ -8,10 +8,10 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20 let(:routes) { table(:routes) } let(:projects) { table(:projects) } - let(:user) { users.create(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') } + let(:user) { users.create!(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') } let(:namespace) do - namespaces.create( + namespaces.create!( owner_id: user.id, name: "Should eventually be the user's name", path: user.username @@ -19,11 +19,11 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20 end let(:project) do - projects.create(namespace_id: namespace.id, name: 'Project Name') + projects.create!(namespace_id: namespace.id, name: 'Project Name') end it "updates the route for a project if it did not match the user's name" do - route = routes.create( + route = routes.create!( id: 1, path: "#{user.username}/#{project.path}", source_id: project.id, @@ -37,7 +37,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20 end it 'updates the route for a project if the name was nil' do - route = routes.create( + route = routes.create!( id: 1, path: "#{user.username}/#{project.path}", source_id: project.id, @@ -51,7 +51,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20 end it 'does not update routes that were are out of the range' do - route = routes.create( + route = routes.create!( id: 6, path: "#{user.username}/#{project.path}", source_id: project.id, @@ -64,14 +64,14 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20 end it 'does not update routes for projects in groups owned by the user' do - group = namespaces.create( + group = namespaces.create!( owner_id: user.id, name: 'A group', path: 'a-path', type: '' ) - project = projects.create(namespace_id: group.id, name: 'Project Name') - route = routes.create( + project = projects.create!(namespace_id: group.id, name: 'Project Name') + route = routes.create!( id: 1, path: "#{group.path}/#{project.path}", source_id: project.id, @@ -84,7 +84,7 @@ RSpec.describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, schema: 20 end it 'does not update routes for namespaces' do - route = routes.create( + route = routes.create!( id: 1, path: namespace.path, source_id: namespace.id, diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb index 934ab7e37f8..264faa4de3b 100644 --- a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb +++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur if with_file upload = create(:upload, :with_file, :attachment_upload, params) - model.update(attachment: upload.retrieve_uploader) + model.update!(attachment: upload.retrieve_uploader) model.attachment.upload else create(:upload, :attachment_upload, params) @@ -245,7 +245,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur end let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) } - let(:bucket) { connection.directories.create(key: 'uploads') } + let(:bucket) { connection.directories.create(key: 'uploads') } # rubocop:disable Rails/SaveBang before do stub_uploads_object_storage(FileUploader) @@ -257,7 +257,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur context 'when the file belongs to a legacy project' do before do - bucket.files.create(remote_file) + bucket.files.create(remote_file) # rubocop:disable Rails/SaveBang end let(:project) { legacy_project } @@ -267,7 +267,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadMover, :aggregate_failur context 'when the file belongs to a hashed project' do before do - bucket.files.create(remote_file) + bucket.files.create(remote_file) # rubocop:disable Rails/SaveBang end let(:project) { hashed_project } diff --git a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb index 66a1787b2cb..7227f80a062 100644 --- a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb +++ b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Gitlab::BackgroundMigration::LegacyUploadsMigrator do if with_file upload = create(:upload, :with_file, :attachment_upload, params) - model.update(attachment: upload.retrieve_uploader) + model.update!(attachment: upload.retrieve_uploader) model.attachment.upload else create(:upload, :attachment_upload, params) diff --git a/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb b/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb index dda4f5a3a36..b7cf101dd8a 100644 --- a/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb +++ b/spec/lib/gitlab/background_migration/link_lfs_objects_projects_spec.rb @@ -10,44 +10,44 @@ RSpec.describe Gitlab::BackgroundMigration::LinkLfsObjectsProjects, :migration, let(:lfs_objects) { table(:lfs_objects) } let(:lfs_objects_projects) { table(:lfs_objects_projects) } - let(:namespace) { namespaces.create(name: 'GitLab', path: 'gitlab') } + let(:namespace) { namespaces.create!(name: 'GitLab', path: 'gitlab') } - let(:fork_network) { fork_networks.create(root_project_id: source_project.id) } - let(:another_fork_network) { fork_networks.create(root_project_id: another_source_project.id) } + let(:fork_network) { fork_networks.create!(root_project_id: source_project.id) } + let(:another_fork_network) { fork_networks.create!(root_project_id: another_source_project.id) } - let(:source_project) { projects.create(namespace_id: namespace.id) } - let(:another_source_project) { projects.create(namespace_id: namespace.id) } - let(:project) { projects.create(namespace_id: namespace.id) } - let(:another_project) { projects.create(namespace_id: namespace.id) } - let(:partially_linked_project) { projects.create(namespace_id: namespace.id) } - let(:fully_linked_project) { projects.create(namespace_id: namespace.id) } + let(:source_project) { projects.create!(namespace_id: namespace.id) } + let(:another_source_project) { projects.create!(namespace_id: namespace.id) } + let(:project) { projects.create!(namespace_id: namespace.id) } + let(:another_project) { projects.create!(namespace_id: namespace.id) } + let(:partially_linked_project) { projects.create!(namespace_id: namespace.id) } + let(:fully_linked_project) { projects.create!(namespace_id: namespace.id) } - let(:lfs_object) { lfs_objects.create(oid: 'abc123', size: 100) } - let(:another_lfs_object) { lfs_objects.create(oid: 'def456', size: 200) } + let(:lfs_object) { lfs_objects.create!(oid: 'abc123', size: 100) } + let(:another_lfs_object) { lfs_objects.create!(oid: 'def456', size: 200) } let!(:source_project_lop_1) do - lfs_objects_projects.create( + lfs_objects_projects.create!( lfs_object_id: lfs_object.id, project_id: source_project.id ) end let!(:source_project_lop_2) do - lfs_objects_projects.create( + lfs_objects_projects.create!( lfs_object_id: another_lfs_object.id, project_id: source_project.id ) end let!(:another_source_project_lop_1) do - lfs_objects_projects.create( + lfs_objects_projects.create!( lfs_object_id: lfs_object.id, project_id: another_source_project.id ) end let!(:another_source_project_lop_2) do - lfs_objects_projects.create( + lfs_objects_projects.create!( lfs_object_id: another_lfs_object.id, project_id: another_source_project.id ) @@ -57,23 +57,23 @@ RSpec.describe Gitlab::BackgroundMigration::LinkLfsObjectsProjects, :migration, stub_const("#{described_class}::BATCH_SIZE", 2) # Create links between projects - fork_network_members.create(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil) + fork_network_members.create!(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil) [project, partially_linked_project, fully_linked_project].each do |p| - fork_network_members.create( + fork_network_members.create!( fork_network_id: fork_network.id, project_id: p.id, forked_from_project_id: fork_network.root_project_id ) end - fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil) - fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_project.id, forked_from_project_id: another_fork_network.root_project_id) + fork_network_members.create!(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil) + fork_network_members.create!(fork_network_id: another_fork_network.id, project_id: another_project.id, forked_from_project_id: another_fork_network.root_project_id) # Links LFS objects to some projects - lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: fully_linked_project.id) - lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: fully_linked_project.id) - lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: partially_linked_project.id) + lfs_objects_projects.create!(lfs_object_id: lfs_object.id, project_id: fully_linked_project.id) + lfs_objects_projects.create!(lfs_object_id: another_lfs_object.id, project_id: fully_linked_project.id) + lfs_objects_projects.create!(lfs_object_id: lfs_object.id, project_id: partially_linked_project.id) end context 'when there are LFS objects to be linked' do @@ -96,8 +96,8 @@ RSpec.describe Gitlab::BackgroundMigration::LinkLfsObjectsProjects, :migration, before do # Links LFS objects to all projects projects.all.each do |p| - lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id) - lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id) + lfs_objects_projects.create!(lfs_object_id: lfs_object.id, project_id: p.id) + lfs_objects_projects.create!(lfs_object_id: another_lfs_object.id, project_id: p.id) end end diff --git a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb index d829fd5daf5..8668216d014 100644 --- a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb @@ -97,7 +97,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'with Jira service' do let!(:service) do - services.create(id: 10, type: 'JiraService', title: nil, properties: jira_properties.to_json, category: 'issue_tracker') + services.create!(id: 10, type: 'JiraService', title: nil, properties: jira_properties.to_json, category: 'issue_tracker') end it_behaves_like 'handle properties' @@ -119,7 +119,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'with bugzilla service' do let!(:service) do - services.create(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker') + services.create!(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker') end it_behaves_like 'handle properties' @@ -140,7 +140,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'with youtrack service' do let!(:service) do - services.create(id: 12, type: 'YoutrackService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker') + services.create!(id: 12, type: 'YoutrackService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker') end it_behaves_like 'handle properties' @@ -161,7 +161,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'with gitlab service with no properties' do let!(:service) do - services.create(id: 13, type: 'GitlabIssueTrackerService', title: nil, properties: {}, category: 'issue_tracker') + services.create!(id: 13, type: 'GitlabIssueTrackerService', title: nil, properties: {}, category: 'issue_tracker') end it_behaves_like 'handle properties' @@ -173,7 +173,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'with redmine service already with data fields' do let!(:service) do - services.create(id: 14, type: 'RedmineService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker').tap do |service| + services.create!(id: 14, type: 'RedmineService', title: nil, properties: tracker_properties_no_url.to_json, category: 'issue_tracker').tap do |service| IssueTrackerData.create!(service_id: service.id, project_url: url, new_issue_url: new_issue_url, issues_url: issues_url) end end @@ -187,7 +187,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'with custom issue tracker which has data fields record inconsistent with properties field' do let!(:service) do - services.create(id: 15, type: 'CustomIssueTrackerService', title: 'Existing title', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service| + services.create!(id: 15, type: 'CustomIssueTrackerService', title: 'Existing title', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service| IssueTrackerData.create!(service_id: service.id, project_url: 'http://other_url', new_issue_url: 'http://other_url/new_issue', issues_url: 'http://other_url/issues') end end @@ -209,7 +209,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'with Jira service which has data fields record inconsistent with properties field' do let!(:service) do - services.create(id: 16, type: 'CustomIssueTrackerService', description: 'Existing description', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service| + services.create!(id: 16, type: 'CustomIssueTrackerService', description: 'Existing description', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service| JiraTrackerData.create!(service_id: service.id, url: 'http://other_jira_url') end end @@ -232,7 +232,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'non issue tracker service' do let!(:service) do - services.create(id: 17, title: nil, description: nil, type: 'OtherService', properties: tracker_properties.to_json) + services.create!(id: 17, title: nil, description: nil, type: 'OtherService', properties: tracker_properties.to_json) end it_behaves_like 'handle properties' @@ -248,7 +248,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'Jira service with empty properties' do let!(:service) do - services.create(id: 18, type: 'JiraService', properties: '', category: 'issue_tracker') + services.create!(id: 18, type: 'JiraService', properties: '', category: 'issue_tracker') end it_behaves_like 'handle properties' @@ -260,7 +260,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'Jira service with nil properties' do let!(:service) do - services.create(id: 18, type: 'JiraService', properties: nil, category: 'issue_tracker') + services.create!(id: 18, type: 'JiraService', properties: nil, category: 'issue_tracker') end it_behaves_like 'handle properties' @@ -272,7 +272,7 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'Jira service with invalid properties' do let!(:service) do - services.create(id: 18, type: 'JiraService', properties: 'invalid data', category: 'issue_tracker') + services.create!(id: 18, type: 'JiraService', properties: 'invalid data', category: 'issue_tracker') end it_behaves_like 'handle properties' @@ -284,15 +284,15 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, s context 'with Jira service with invalid properties, valid Jira service and valid bugzilla service' do let!(:jira_service_invalid) do - services.create(id: 19, title: 'invalid - title', description: 'invalid - description', type: 'JiraService', properties: 'invalid data', category: 'issue_tracker') + services.create!(id: 19, title: 'invalid - title', description: 'invalid - description', type: 'JiraService', properties: 'invalid data', category: 'issue_tracker') end let!(:jira_service_valid) do - services.create(id: 20, type: 'JiraService', properties: jira_properties.to_json, category: 'issue_tracker') + services.create!(id: 20, type: 'JiraService', properties: jira_properties.to_json, category: 'issue_tracker') end let!(:bugzilla_service_valid) do - services.create(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker') + services.create!(id: 11, type: 'BugzillaService', title: nil, properties: tracker_properties.to_json, category: 'issue_tracker') end it 'migrates data for the valid service' do diff --git a/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb b/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb index 3cec5cb4c35..d90a5d30954 100644 --- a/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_users_bio_to_user_details_spec.rb @@ -11,17 +11,17 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateUsersBioToUserDetails, :migra klass end - let!(:user_needs_migration) { users.create(name: 'user1', email: 'test1@test.com', projects_limit: 1, bio: 'bio') } - let!(:user_needs_no_migration) { users.create(name: 'user2', email: 'test2@test.com', projects_limit: 1) } - let!(:user_also_needs_no_migration) { users.create(name: 'user3', email: 'test3@test.com', projects_limit: 1, bio: '') } - let!(:user_with_long_bio) { users.create(name: 'user4', email: 'test4@test.com', projects_limit: 1, bio: 'a' * 256) } # 255 is the max + let!(:user_needs_migration) { users.create!(name: 'user1', email: 'test1@test.com', projects_limit: 1, bio: 'bio') } + let!(:user_needs_no_migration) { users.create!(name: 'user2', email: 'test2@test.com', projects_limit: 1) } + let!(:user_also_needs_no_migration) { users.create!(name: 'user3', email: 'test3@test.com', projects_limit: 1, bio: '') } + let!(:user_with_long_bio) { users.create!(name: 'user4', email: 'test4@test.com', projects_limit: 1, bio: 'a' * 256) } # 255 is the max - let!(:user_already_has_details) { users.create(name: 'user5', email: 'test5@test.com', projects_limit: 1, bio: 'my bio') } - let!(:existing_user_details) { user_details.find_or_create_by(user_id: user_already_has_details.id).update(bio: 'my bio') } + let!(:user_already_has_details) { users.create!(name: 'user5', email: 'test5@test.com', projects_limit: 1, bio: 'my bio') } + let!(:existing_user_details) { user_details.find_or_create_by!(user_id: user_already_has_details.id).update!(bio: 'my bio') } # unlikely scenario since we have triggers - let!(:user_has_different_details) { users.create(name: 'user6', email: 'test6@test.com', projects_limit: 1, bio: 'different') } - let!(:different_existing_user_details) { user_details.find_or_create_by(user_id: user_has_different_details.id).update(bio: 'bio') } + let!(:user_has_different_details) { users.create!(name: 'user6', email: 'test6@test.com', projects_limit: 1, bio: 'different') } + let!(:different_existing_user_details) { user_details.find_or_create_by!(user_id: user_has_different_details.id).update!(bio: 'bio') } let(:user_ids) do [ diff --git a/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb b/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb index ee0024e8526..36000dc3ffd 100644 --- a/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_canonical_emails_spec.rb @@ -63,7 +63,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateCanonicalEmails, :migration, describe 'gracefully handles existing records, some of which may have an already-existing identical canonical_email field' do let_it_be(:user_one) { create_user(email: "example.user@gmail.com", id: 1) } let_it_be(:user_two) { create_user(email: "exampleuser@gmail.com", id: 2) } - let_it_be(:user_email_one) { user_canonical_emails.create(canonical_email: "exampleuser@gmail.com", user_id: user_one.id) } + let_it_be(:user_email_one) { user_canonical_emails.create!(canonical_email: "exampleuser@gmail.com", user_id: user_one.id) } subject { migration.perform(1, 2) } @@ -79,7 +79,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateCanonicalEmails, :migration, projects_limit: 0 } - users.create(default_attributes.merge!(attributes)) + users.create!(default_attributes.merge!(attributes)) end def canonical_emails(user_id: nil) diff --git a/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb new file mode 100644 index 00000000000..bc55f240a58 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_dismissed_state_for_vulnerabilities_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::BackgroundMigration::PopulateDismissedStateForVulnerabilities, schema: 2020_11_30_103926 do + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:vulnerabilities) { table(:vulnerabilities) } + + let!(:namespace) { namespaces.create!(name: "foo", path: "bar") } + let!(:user) { users.create!(name: 'John Doe', email: 'test@example.com', projects_limit: 5) } + let!(:project) { projects.create!(namespace_id: namespace.id) } + let!(:vulnerability_params) do + { + project_id: project.id, + author_id: user.id, + title: 'Vulnerability', + severity: 5, + confidence: 5, + report_type: 5 + } + end + + let!(:vulnerability_1) { vulnerabilities.create!(vulnerability_params.merge(state: 1)) } + let!(:vulnerability_2) { vulnerabilities.create!(vulnerability_params.merge(state: 3)) } + + describe '#perform' do + it 'changes state of vulnerability to dismissed' do + subject.perform(vulnerability_1.id, vulnerability_2.id) + + expect(vulnerability_1.reload.state).to eq(2) + expect(vulnerability_2.reload.state).to eq(2) + end + + it 'populates missing dismissal information' do + expect_next_instance_of(::Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation) do |migration| + expect(migration).to receive(:perform).with(vulnerability_1.id, vulnerability_2.id) + end + + subject.perform(vulnerability_1.id, vulnerability_2.id) + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb index 1e5773ee16b..4e7872a9a1b 100644 --- a/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb @@ -11,8 +11,8 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateMergeRequestAssigneesTable, let(:user_2) { users.create!(email: 'test2@example.com', projects_limit: 100, username: 'test') } let(:user_3) { users.create!(email: 'test3@example.com', projects_limit: 100, username: 'test') } - let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } - let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') } let(:merge_requests) { table(:merge_requests) } let(:merge_request_assignees) { table(:merge_request_assignees) } @@ -24,7 +24,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateMergeRequestAssigneesTable, source_branch: 'mr name', title: "mr name#{id}") - merge_requests.create(params) + merge_requests.create!(params) end before do diff --git a/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb b/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb index f0b0f77280e..b3cacc60cdc 100644 --- a/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_user_highest_roles_table_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateUserHighestRolesTable, schem projects_limit: 0 }.merge(params) - users.create(user_params) + users.create!(user_params) end def create_member(id, access_level, params = {}) @@ -30,7 +30,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateUserHighestRolesTable, schem notification_level: 0 }.merge(params) - members.create(params) + members.create!(params) end before do @@ -47,7 +47,7 @@ RSpec.describe Gitlab::BackgroundMigration::PopulateUserHighestRolesTable, schem create_member(7, 30) create_member(8, 20, requested_at: Time.current) - user_highest_roles.create(user_id: 1, highest_access_level: 50) + user_highest_roles.create!(user_id: 1, highest_access_level: 50) end describe '#perform' do diff --git a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb index 33e1f31d1f1..1c55b50ea3f 100644 --- a/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb +++ b/spec/lib/gitlab/background_migration/recalculate_project_authorizations_spec.rb @@ -91,7 +91,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, sc projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0, namespace_id: shared_group.id) - group_group_links.create(shared_group_id: shared_group.id, shared_with_group_id: group.id, + group_group_links.create!(shared_group_id: shared_group.id, shared_with_group_id: group.id, group_access: 20) end @@ -111,7 +111,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, sc shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project', visibility_level: 0, namespace_id: another_group.id) - project_group_links.create(project_id: shared_project.id, group_id: group.id, group_access: 20) + project_group_links.create!(project_id: shared_project.id, group_id: group.id, group_access: 20) end it 'creates correct authorization' do @@ -174,7 +174,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, sc projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0, namespace_id: shared_group.id) - group_group_links.create(shared_group_id: shared_group.id, shared_with_group_id: group.id, + group_group_links.create!(shared_group_id: shared_group.id, shared_with_group_id: group.id, group_access: 20) end @@ -192,7 +192,7 @@ RSpec.describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, sc shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project', visibility_level: 0, namespace_id: another_group.id) - project_group_links.create(project_id: shared_project.id, group_id: group.id, group_access: 20) + project_group_links.create!(project_id: shared_project.id, group_id: group.id, group_access: 20) end it 'does not create authorization' do diff --git a/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb index 43fc0fb3691..2f5074649c4 100644 --- a/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb +++ b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb @@ -5,8 +5,8 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::ResetMergeStatus do let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } - let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } - let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') } let(:merge_requests) { table(:merge_requests) } def create_merge_request(id, extra_params = {}) diff --git a/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb b/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb new file mode 100644 index 00000000000..bebb398413b --- /dev/null +++ b/spec/lib/gitlab/background_migration/update_existing_users_that_require_two_factor_auth_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::UpdateExistingUsersThatRequireTwoFactorAuth, schema: 20201030121314 do + include MigrationHelpers::NamespacesHelpers + + let(:group_with_2fa_parent) { create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE) } + let(:group_with_2fa_child) { create_namespace('child', Gitlab::VisibilityLevel::PRIVATE, parent_id: group_with_2fa_parent.id) } + let(:members_table) { table(:members) } + let(:users_table) { table(:users) } + + subject { described_class.new } + + describe '#perform' do + context 'with group members' do + let(:user_1) { create_user('user@example.com') } + let!(:member) { create_group_member(user_1, group_with_2fa_parent) } + let!(:user_without_group) { create_user('user_without@example.com') } + let(:user_other) { create_user('user_other@example.com') } + let!(:member_other) { create_group_member(user_other, group_with_2fa_parent) } + + it 'updates user when user should not be required to establish two factor authentication' do + subject.perform(user_1.id, user_without_group.id) + + expect(user_1.reload.require_two_factor_authentication_from_group).to eq(false) + end + + it 'does not update user when user is member of group that requires two factor authentication' do + group = create_namespace('other', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true) + create_group_member(user_1, group) + + subject.perform(user_1.id, user_without_group.id) + + expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true) + end + + it 'does not update user who is not in current batch' do + subject.perform(user_1.id, user_without_group.id) + + expect(user_other.reload.require_two_factor_authentication_from_group).to eq(true) + end + + it 'updates all users in current batch' do + subject.perform(user_1.id, user_other.id) + + expect(user_other.reload.require_two_factor_authentication_from_group).to eq(false) + end + + it 'does not update user when user is member of group which parent group requires two factor authentication' do + group_with_2fa_parent.update!(require_two_factor_authentication: true) + subject.perform(user_1.id, user_other.id) + + expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true) + end + + it 'does not update user when user is member of group which has subgroup that requires two factor authentication' do + create_namespace('subgroup', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true, parent_id: group_with_2fa_child.id) + + subject.perform(user_1.id, user_other.id) + + expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true) + end + end + end + + def create_user(email, require_2fa: true) + users_table.create!(email: email, projects_limit: 10, require_two_factor_authentication_from_group: require_2fa) + end + + def create_group_member(user, group) + members_table.create!(user_id: user.id, source_id: group.id, access_level: GroupMember::MAINTAINER, source_type: "Namespace", type: "GroupMember", notification_level: 3) + end +end diff --git a/spec/lib/gitlab/checks/diff_check_spec.rb b/spec/lib/gitlab/checks/diff_check_spec.rb index 2cca0aed9c6..f4daafb1d0e 100644 --- a/spec/lib/gitlab/checks/diff_check_spec.rb +++ b/spec/lib/gitlab/checks/diff_check_spec.rb @@ -7,7 +7,6 @@ RSpec.describe Gitlab::Checks::DiffCheck do describe '#validate!' do let(:owner) { create(:user) } - let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') } before do allow(project.repository).to receive(:new_commits).and_return( @@ -28,13 +27,27 @@ RSpec.describe Gitlab::Checks::DiffCheck do end context 'with LFS enabled' do + let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') } + before do allow(project).to receive(:lfs_enabled?).and_return(true) end context 'when change is sent by a different user' do - it 'raises an error if the user is not allowed to update the file' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}") + context 'when diff check with paths rpc feature flag is true' do + it 'raises an error if the user is not allowed to update the file' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}") + end + end + + context 'when diff check with paths rpc feature flag is false' do + before do + stub_feature_flags(diff_check_with_paths_changed_rpc: false) + end + + it 'raises an error if the user is not allowed to update the file' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}") + end end end @@ -53,6 +66,8 @@ RSpec.describe Gitlab::Checks::DiffCheck do expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original + stub_feature_flags(diff_check_with_paths_changed_rpc: false) + subject.validate! end diff --git a/spec/lib/gitlab/checks/push_check_spec.rb b/spec/lib/gitlab/checks/push_check_spec.rb index 45ab13cf0cf..262438256b4 100644 --- a/spec/lib/gitlab/checks/push_check_spec.rb +++ b/spec/lib/gitlab/checks/push_check_spec.rb @@ -18,5 +18,26 @@ RSpec.describe Gitlab::Checks::PushCheck do expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.') end end + + context 'when using a DeployKeyAccess instance' do + let(:deploy_key) { create(:deploy_key) } + let(:user_access) { Gitlab::DeployKeyAccess.new(deploy_key, container: project) } + + context 'when the deploy key cannot push to the targetted branch' do + it 'raises an error' do + allow(user_access).to receive(:can_push_to_branch?).and_return(false) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.') + end + end + + context 'when the deploy key can push to the targetted branch' do + it 'is valid' do + allow(user_access).to receive(:can_push_to_branch?).and_return(true) + + expect { subject.validate! }.not_to raise_error + end + end + end end end diff --git a/spec/lib/gitlab/checks/snippet_check_spec.rb b/spec/lib/gitlab/checks/snippet_check_spec.rb index 037de8e9369..89417aaca4d 100644 --- a/spec/lib/gitlab/checks/snippet_check_spec.rb +++ b/spec/lib/gitlab/checks/snippet_check_spec.rb @@ -9,19 +9,30 @@ RSpec.describe Gitlab::Checks::SnippetCheck do let(:user_access) { Gitlab::UserAccessSnippet.new(user, snippet: snippet) } let(:default_branch) { snippet.default_branch } + let(:branch_name) { default_branch } + let(:creation) { false } + let(:deletion) { false } - subject { Gitlab::Checks::SnippetCheck.new(changes, default_branch: default_branch, logger: logger) } + subject { Gitlab::Checks::SnippetCheck.new(changes, default_branch: default_branch, root_ref: snippet.repository.root_ref, logger: logger) } describe '#validate!' do it 'does not raise any error' do expect { subject.validate! }.not_to raise_error end + shared_examples 'raises and logs error' do + specify do + expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(Gitlab::GitAccess::ForbiddenError), default_branch: default_branch, branch_name: branch_name, creation: creation, deletion: deletion) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.') + end + end + context 'trying to delete the branch' do let(:newrev) { '0000000000000000000000000000000000000000' } - it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.') + it_behaves_like 'raises and logs error' do + let(:deletion) { true } end end @@ -29,14 +40,23 @@ RSpec.describe Gitlab::Checks::SnippetCheck do let(:oldrev) { '0000000000000000000000000000000000000000' } let(:ref) { 'refs/heads/feature' } - it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.') + it_behaves_like 'raises and logs error' do + let(:creation) { true } + let(:branch_name) { 'feature' } end - context "when branch is 'master'" do - let(:ref) { 'refs/heads/master' } + context 'when branch is the same as the default branch' do + let(:ref) { "refs/heads/#{default_branch}" } - it "allows the operation" do + it 'allows the operation' do + expect { subject.validate! }.not_to raise_error + end + end + + context 'when snippet has an empty repo' do + let_it_be(:snippet) { create(:personal_snippet, :empty_repo) } + + it 'allows the operation' do expect { subject.validate! }.not_to raise_error end end @@ -45,8 +65,8 @@ RSpec.describe Gitlab::Checks::SnippetCheck do context 'when default_branch is nil' do let(:default_branch) { nil } - it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You can not create or delete branches.') + it_behaves_like 'raises and logs error' do + let(:branch_name) { 'master' } end end end diff --git a/spec/lib/gitlab/ci/ansi2json/result_spec.rb b/spec/lib/gitlab/ci/ansi2json/result_spec.rb index 31c0da95f0a..b7b4d6de8b9 100644 --- a/spec/lib/gitlab/ci/ansi2json/result_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json/result_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::Result do { lines: [], state: state, append: false, truncated: false, offset: offset, stream: stream } end - subject { described_class.new(params) } + subject { described_class.new(**params) } describe '#size' do before do diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb index d27a642ecf3..ff70ff69aaa 100644 --- a/spec/lib/gitlab/ci/ansi2json/style_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Ansi2json::Style do describe '#set?' do - subject { described_class.new(params).set? } + subject { described_class.new(**params).set? } context 'when fg color is set' do let(:params) { { fg: 'term-fg-black' } } @@ -44,7 +44,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::Style do end describe 'update formats to mimic terminals' do - subject { described_class.new(params) } + subject { described_class.new(**params) } context 'when fg color present' do let(:params) { { fg: 'term-fg-black', mask: mask } } diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb index 77b8aa1d591..efe99cd276c 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -142,7 +142,7 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata do it 'reads expected number of entries' do stream = File.open(tmpfile.path) - metadata = described_class.new(stream, 'public', { recursive: true }) + metadata = described_class.new(stream, 'public', recursive: true) expect(metadata.find_entries!.count).to eq entry_count end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause_spec.rb new file mode 100644 index 00000000000..faede7a361f --- /dev/null +++ b/spec/lib/gitlab/ci/build/rules/rule/clause_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause do + describe '.fabricate' do + using RSpec::Parameterized::TableSyntax + + let(:value) { 'some value' } + + subject { described_class.fabricate(type, value) } + + context 'when type is valid' do + where(:type, :result) do + 'changes' | Gitlab::Ci::Build::Rules::Rule::Clause::Changes + 'exists' | Gitlab::Ci::Build::Rules::Rule::Clause::Exists + 'if' | Gitlab::Ci::Build::Rules::Rule::Clause::If + end + + with_them do + it { is_expected.to be_instance_of(result) } + end + end + + context 'when type is invalid' do + let(:type) { 'when' } + + it { is_expected.to be_nil } + + context "when type is 'variables'" do + let(:type) { 'variables' } + + it { is_expected.to be_nil } + end + end + end +end diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb index cbeae33fbcf..a1af5b75f87 100644 --- a/spec/lib/gitlab/ci/build/rules_spec.rb +++ b/spec/lib/gitlab/ci/build/rules_spec.rb @@ -104,7 +104,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do context 'with one rule without any clauses' do let(:rule_list) { [{ when: 'manual', allow_failure: true }] } - it { is_expected.to eq(described_class::Result.new('manual', nil, true)) } + it { is_expected.to eq(described_class::Result.new('manual', nil, true, nil)) } end context 'with one matching rule' do @@ -171,7 +171,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do context 'with matching rule' do let(:rule_list) { [{ if: '$VAR == null', allow_failure: true }] } - it { is_expected.to eq(described_class::Result.new('on_success', nil, true)) } + it { is_expected.to eq(described_class::Result.new('on_success', nil, true, nil)) } end context 'with non-matching rule' do @@ -180,18 +180,60 @@ RSpec.describe Gitlab::Ci::Build::Rules do it { is_expected.to eq(described_class::Result.new('never')) } end end + + context 'with variables' do + context 'with matching rule' do + let(:rule_list) { [{ if: '$VAR == null', variables: { MY_VAR: 'my var' } }] } + + it { is_expected.to eq(described_class::Result.new('on_success', nil, nil, { MY_VAR: 'my var' })) } + end + end end describe 'Gitlab::Ci::Build::Rules::Result' do let(:when_value) { 'on_success' } let(:start_in) { nil } let(:allow_failure) { nil } + let(:variables) { nil } - subject { Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure) } + subject(:result) do + Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure, variables) + end describe '#build_attributes' do + let(:seed_attributes) { {} } + + subject(:build_attributes) do + result.build_attributes(seed_attributes) + end + it 'compacts nil values' do - expect(subject.build_attributes).to eq(options: {}, when: 'on_success') + is_expected.to eq(options: {}, when: 'on_success') + end + + context 'when there are variables in rules' do + let(:variables) { { VAR1: 'new var 1', VAR3: 'var 3' } } + + context 'when there are seed variables' do + let(:seed_attributes) do + { yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }, + { key: 'VAR2', value: 'var 2', public: true }] } + end + + it 'returns yaml_variables with override' do + is_expected.to include( + yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true }, + { key: 'VAR2', value: 'var 2', public: true }, + { key: 'VAR3', value: 'var 3', public: true }] + ) + end + end + + context 'when there is not seed variables' do + it 'does not return yaml_variables' do + is_expected.not_to have_key(:yaml_variables) + end + end end end @@ -200,7 +242,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do let!(:when_value) { 'never' } it 'returns false' do - expect(subject.pass?).to eq(false) + expect(result.pass?).to eq(false) end end @@ -208,7 +250,7 @@ RSpec.describe Gitlab::Ci::Build::Rules do let!(:when_value) { 'on_success' } it 'returns true' do - expect(subject.pass?).to eq(true) + expect(result.pass?).to eq(true) end end end diff --git a/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb b/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb new file mode 100644 index 00000000000..7aaad57f5cd --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/allow_failure_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::AllowFailure do + let(:entry) { described_class.new(config.deep_dup) } + let(:expected_config) { config } + + describe 'validations' do + context 'when entry config value is valid' do + shared_examples 'valid entry' do + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq(expected_config) + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'with boolean values' do + it_behaves_like 'valid entry' do + let(:config) { true } + end + + it_behaves_like 'valid entry' do + let(:config) { false } + end + end + + context 'with hash values' do + it_behaves_like 'valid entry' do + let(:config) { { exit_codes: 137 } } + let(:expected_config) { { exit_codes: [137] } } + end + + it_behaves_like 'valid entry' do + let(:config) { { exit_codes: [42, 137] } } + end + end + end + + context 'when entry value is not valid' do + shared_examples 'invalid entry' do + describe '#valid?' do + it { expect(entry).not_to be_valid } + it { expect(entry.errors).to include(error_message) } + end + end + + context 'when it has a wrong type' do + let(:config) { [1] } + let(:error_message) do + 'allow failure config should be a hash or a boolean value' + end + + it_behaves_like 'invalid entry' + end + + context 'with string exit codes' do + let(:config) { { exit_codes: 'string' } } + let(:error_message) do + 'allow failure exit codes should be an array of integers or an integer' + end + + it_behaves_like 'invalid entry' + end + + context 'with array of strings as exit codes' do + let(:config) { { exit_codes: ['string 1', 'string 2'] } } + let(:error_message) do + 'allow failure exit codes should be an array of integers or an integer' + end + + it_behaves_like 'invalid entry' + end + + context 'when it has an extra keys' do + let(:config) { { extra: true } } + let(:error_message) do + 'allow failure config contains unknown keys: extra' + end + + it_behaves_like 'invalid entry' + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index 8b2e0410474..b3b7901074a 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -227,6 +227,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do end end end + + context 'when bridge config contains exit_codes' do + let(:config) do + { script: 'rspec', allow_failure: { exit_codes: [42] } } + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns an error message' do + expect(subject.errors) + .to include(/allow failure should be a boolean value/) + end + end + end end describe '#manual_action?' do diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index c3d91057328..e810d65d560 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -81,7 +81,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do context 'when configuration has ports' do let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } let(:config) { { name: 'ruby:2.7', entrypoint: %w(/bin/sh run), ports: ports } } - let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:entry) { described_class.new(config, with_image_ports: image_ports) } let(:image_ports) { false } context 'when with_image_ports metadata is not enabled' do diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index e0e8bc93770..7834a1a94f2 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -670,6 +670,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do end describe '#ignored?' do + before do + entry.compose! + end + context 'when job is a manual action' do context 'when it is not specified if job is allowed to fail' do let(:config) do @@ -700,6 +704,16 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do expect(entry).not_to be_ignored end end + + context 'when job is dynamically allowed to fail' do + let(:config) do + { script: 'deploy', when: 'manual', allow_failure: { exit_codes: 42 } } + end + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + end end context 'when job is not a manual action' do @@ -709,6 +723,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'is not an ignored job' do expect(entry).not_to be_ignored end + + it 'does not return allow_failure' do + expect(entry.value.key?(:allow_failure_criteria)).to be_falsey + end end context 'when job is allowed to fail' do @@ -717,6 +735,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'is an ignored job' do expect(entry).to be_ignored end + + it 'does not return allow_failure_criteria' do + expect(entry.value.key?(:allow_failure_criteria)).to be_falsey + end end context 'when job is not allowed to fail' do @@ -725,6 +747,32 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do it 'is not an ignored job' do expect(entry).not_to be_ignored end + + it 'does not return allow_failure_criteria' do + expect(entry.value.key?(:allow_failure_criteria)).to be_falsey + end + end + + context 'when job is dynamically allowed to fail' do + let(:config) { { script: 'deploy', allow_failure: { exit_codes: 42 } } } + + it 'is not an ignored job' do + expect(entry).not_to be_ignored + end + + it 'returns allow_failure_criteria' do + expect(entry.value[:allow_failure_criteria]).to match(exit_codes: [42]) + end + + context 'with ci_allow_failure_with_exit_codes disabled' do + before do + stub_feature_flags(ci_allow_failure_with_exit_codes: false) + end + + it 'does not return allow_failure_criteria' do + expect(entry.value.key?(:allow_failure_criteria)).to be_falsey + end + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb index 5a826bf8282..983e95fae42 100644 --- a/spec/lib/gitlab/ci/config/entry/need_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb @@ -165,6 +165,45 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do end end + context 'with cross pipeline artifacts needs' do + context 'when pipeline is provided' do + context 'when job is provided' do + let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID' } } + + it { is_expected.to be_valid } + + it 'sets artifacts:true by default' do + expect(need.value).to eq(job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: true) + end + + it 'sets the type as cross_dependency' do + expect(need.type).to eq(:cross_dependency) + end + end + + context 'when artifacts is provided' do + let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: false } } + + it { is_expected.to be_valid } + + it 'returns the correct value' do + expect(need.value).to eq(job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: false) + end + end + end + + context 'when config contains not allowed keys' do + let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID', something: 'else' } } + + it { is_expected.not_to be_valid } + + it 'returns an error' do + expect(need.errors) + .to contain_exactly('cross pipeline dependency config contains unknown keys: something') + end + end + end + context 'when need config is not a string or a hash' do let(:config) { :job_name } diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb index f3b9d0c3c84..f11f2a56f5f 100644 --- a/spec/lib/gitlab/ci/config/entry/needs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb @@ -6,7 +6,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do subject(:needs) { described_class.new(config) } before do - needs.metadata[:allowed_needs] = %i[job] + needs.metadata[:allowed_needs] = %i[job cross_dependency] end describe 'validations' do @@ -66,6 +66,27 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do end end end + + context 'with too many cross pipeline dependencies' do + let(:limit) { described_class::NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT } + + let(:config) do + Array.new(limit.next) do |index| + { pipeline: "$UPSTREAM_PIPELINE_#{index}", job: 'job-1' } + end + end + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about incorrect type' do + expect(needs.errors).to contain_exactly( + "needs config must be less than or equal to #{limit}") + end + end + end end describe '.compose!' do diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index ac8dd2a3267..aadf94365c6 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -361,7 +361,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do context 'when root yaml variables are used' do let(:variables) do Gitlab::Ci::Config::Entry::Variables.new( - A: 'root', C: 'root', D: 'root' + { A: 'root', C: 'root', D: 'root' } ).value end diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 79716df6b60..54c7a5c3602 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: 'ruby:2.7', default: {}, services: ['postgres:9.1', 'mysql:5.5'], - variables: { VAR: 'root' }, + variables: { VAR: 'root', VAR2: { value: 'val 2', description: 'this is var 2' } }, after_script: ['make clean'], stages: %w(build pages release), cache: { key: 'k', untracked: true, paths: ['public/'] }, @@ -80,6 +80,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do .to eq 'List of external YAML files to include.' end + it 'sets correct variables value' do + expect(root.variables_value).to eq('VAR' => 'root', 'VAR2' => 'val 2') + end + describe '#leaf?' do it 'is not leaf' do expect(root).not_to be_leaf @@ -128,7 +132,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, - variables: { 'VAR' => 'root' }, + variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -142,7 +146,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: { key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success' }, - variables: { 'VAR' => 'root' }, + variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, ignore: false, after_script: ['make clean'], only: { refs: %w[branches tags] }, @@ -158,7 +162,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], cache: { key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }, only: { refs: %w(branches tags) }, - variables: { 'VAR' => 'job' }, + variables: { 'VAR' => 'job', 'VAR2' => 'val 2' }, after_script: [], ignore: false, scheduling_type: :stage } diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb index 4a43e6c9a86..d1bd22e5573 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -339,6 +339,22 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do end end end + + context 'with an invalid variables' do + let(:config) do + { if: '$THIS == "that"', variables: 'hello' } + end + + before do + subject.compose! + end + + it { is_expected.not_to be_valid } + + it 'returns an error about invalid variables:' do + expect(subject.errors).to include(/variables config should be a hash of key value pairs/) + end + end end context 'allow_failure: validation' do diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index ec137ef2ae4..2795cc9dddf 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -96,7 +96,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports } end - let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:entry) { described_class.new(config, with_image_ports: image_ports) } let(:image_ports) { false } context 'when with_image_ports metadata is not enabled' do diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb index e4f8a348d21..85e7f297b03 100644 --- a/spec/lib/gitlab/ci/config/entry/services_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Services do context 'when configuration has ports' do let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] } - let(:entry) { described_class.new(config, { with_image_ports: image_ports }) } + let(:entry) { described_class.new(config, with_image_ports: image_ports) } let(:image_ports) { false } context 'when with_image_ports metadata is not enabled' do diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index ac33f858f43..426a38e2ef7 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -3,7 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Variables do - subject { described_class.new(config) } + let(:metadata) { {} } + + subject { described_class.new(config, metadata) } shared_examples 'valid config' do describe '#value' do @@ -71,7 +73,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } end - it_behaves_like 'valid config' + it_behaves_like 'invalid config' + + context 'when metadata has use_value_data' do + let(:metadata) { { use_value_data: true } } + + it_behaves_like 'valid config' + end end context 'when entry value is an array' do @@ -80,32 +88,36 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do it_behaves_like 'invalid config' end - context 'when entry value has hash with other key-pairs' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1', hello: 'variable 1' }, - 'VARIABLE_2' => 'value 2' } - end + context 'when metadata has use_value_data' do + let(:metadata) { { use_value_data: true } } - it_behaves_like 'invalid config' - end + context 'when entry value has hash with other key-pairs' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1', hello: 'variable 1' }, + 'VARIABLE_2' => 'value 2' } + end - context 'when entry config value has hash with nil description' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1', description: nil } } + it_behaves_like 'invalid config' end - it_behaves_like 'invalid config' - end + context 'when entry config value has hash with nil description' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1', description: nil } } + end - context 'when entry config value has hash without description' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1' } } + it_behaves_like 'invalid config' end - let(:result) do - { 'VARIABLE_1' => 'value 1' } - end + context 'when entry config value has hash without description' do + let(:config) do + { 'VARIABLE_1' => { value: 'value 1' } } + end - it_behaves_like 'valid config' + let(:result) do + { 'VARIABLE_1' => 'value 1' } + end + + it_behaves_like 'valid config' + end end end diff --git a/spec/lib/gitlab/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb index 7b2d6b58518..7d950c86700 100644 --- a/spec/lib/gitlab/ci/mask_secret_spec.rb +++ b/spec/lib/gitlab/ci/mask_secret_spec.rb @@ -22,6 +22,10 @@ RSpec.describe Gitlab::Ci::MaskSecret do expect(mask('token', nil)).to eq('token') end + it 'does not change a bytesize of a value' do + expect(mask('token-ü/unicode', 'token-ü').bytesize).to eq 16 + end + def mask(value, token) subject.mask!(value.dup, token) end diff --git a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb new file mode 100644 index 00000000000..c6b8cf2a985 --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do + describe '#parse!' do + subject(:parse) { described_class.new.parse!(code_climate, codequality_report) } + + let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new } + let(:code_climate) do + [ + { + "categories": [ + "Complexity" + ], + "check_name": "argument_count", + "content": { + "body": "" + }, + "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", + "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", + "location": { + "path": "foo.rb", + "lines": { + "begin": 10, + "end": 10 + } + }, + "other_locations": [], + "remediation_points": 900000, + "severity": "major", + "type": "issue", + "engine_name": "structure" + } + ].to_json + end + + context "when data is code_climate style JSON" do + context "when there are no degradations" do + let(:code_climate) { [].to_json } + + it "returns a codequality report" do + expect { parse }.not_to raise_error + + expect(codequality_report.degradations_count).to eq(0) + end + end + + context "when there are degradations" do + it "returns a codequality report" do + expect { parse }.not_to raise_error + + expect(codequality_report.degradations_count).to eq(1) + end + end + end + + context "when data is not a valid JSON string" do + let(:code_climate) do + [ + { + "categories": [ + "Complexity" + ], + "check_name": "argument_count", + "content": { + "body": "" + }, + "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", + "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", + "location": { + "path": "foo.rb", + "lines": { + "begin": 10, + "end": 10 + } + }, + "other_locations": [], + "remediation_points": 900000, + "severity": "major", + "type": "issue", + "engine_name": "structure" + } + ] + end + + it "sets error_message" do + expect { parse }.not_to raise_error + + expect(codequality_report.error_message).to include('JSON parsing failed') + end + end + + context 'when degradations contain an invalid one' do + let(:code_climate) do + [ + { + "type": "Issue", + "check_name": "Rubocop/Metrics/ParameterLists", + "description": "Avoid parameter lists longer than 5 parameters. [12/5]", + "fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf", + "severity": "minor" + }, + { + "categories": [ + "Complexity" + ], + "check_name": "argument_count", + "content": { + "body": "" + }, + "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", + "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", + "location": { + "path": "foo.rb", + "lines": { + "begin": 10, + "end": 10 + } + }, + "other_locations": [], + "remediation_points": 900000, + "severity": "major", + "type": "issue", + "engine_name": "structure" + } + ].to_json + end + + it 'stops parsing the report' do + expect { parse }.not_to raise_error + + expect(codequality_report.degradations_count).to eq(0) + expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'") + end + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb index 45e87466532..2313378d1e9 100644 --- a/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb +++ b/spec/lib/gitlab/ci/parsers/coverage/cobertura_spec.rb @@ -4,207 +4,690 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do describe '#parse!' do - subject { described_class.new.parse!(cobertura, coverage_report) } + subject(:parse_report) { described_class.new.parse!(cobertura, coverage_report, project_path: project_path, worktree_paths: paths) } let(:coverage_report) { Gitlab::Ci::Reports::CoverageReports.new } + let(:project_path) { 'foo/bar' } + let(:paths) { ['app/user.rb'] } + + let(:cobertura) do + <<~EOF + <coverage> + #{sources_xml} + #{classes_xml} + </coverage> + EOF + end context 'when data is Cobertura style XML' do - context 'when there is no <class>' do - let(:cobertura) { '' } + shared_examples_for 'ignoring sources, project_path, and worktree_paths' do + context 'when there is no <class>' do + let(:classes_xml) { '' } - it 'parses XML and returns empty coverage' do - expect { subject }.not_to raise_error + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error - expect(coverage_report.files).to eq({}) + expect(coverage_report.files).to eq({}) + end end - end - context 'when there is a <sources>' do - shared_examples_for 'ignoring sources' do - it 'parses XML without errors' do - expect { subject }.not_to raise_error + context 'when there is a single <class>' do + context 'with no lines' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="app.rb"></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end - expect(coverage_report.files).to eq({}) + context 'with a single line' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="app.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) + end + end + + context 'without a package parent' do + let(:classes_xml) do + <<~EOF + <packages> + <class filename="app.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </packages> + EOF + end + + it 'parses XML and returns a single file with coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) + end + end + + context 'with multiple lines and methods info' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="app.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) + end end end - context 'and has a single source' do - let(:cobertura) do - <<-EOF.strip_heredoc + context 'when there are multiple <class>' do + context 'without a package parent' do + let(:classes_xml) do + <<~EOF + <packages> + <class filename="app.rb"><methods/><lines> + <line number="1" hits="2"/> + </lines></class> + <class filename="foo.rb"><methods/><lines> + <line number="6" hits="1"/> + </lines></class> + </packages> + EOF + end + + it 'parses XML and returns coverage information per class' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 }, 'foo.rb' => { 6 => 1 } }) + end + end + + context 'with the same filename and different lines' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="app.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="app.rb"><methods/><lines> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with merged coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) + end + end + + context 'with the same filename and lines' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="app.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="app.rb"><methods/><lines> + <line number="1" hits="1"/> + <line number="2" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with summed-up coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } }) + end + end + + context 'with missing filename' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="app.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class><methods/><lines> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and ignores class with missing name' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) + end + end + + context 'with invalid line information' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="app.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="app.rb"><methods/><lines> + <line null="test" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'raises an error' do + expect { parse_report }.to raise_error(described_class::InvalidLineInformationError) + end + end + end + end + + context 'when there is no <sources>' do + let(:sources_xml) { '' } + + it_behaves_like 'ignoring sources, project_path, and worktree_paths' + end + + context 'when there is a <sources>' do + context 'and has a single source with a pattern for Go projects' do + let(:project_path) { 'local/go' } # Make sure we're not making false positives + let(:sources_xml) do + <<~EOF <sources> - <source>project/src</source> + <source>/usr/local/go/src</source> </sources> EOF end - it_behaves_like 'ignoring sources' + it_behaves_like 'ignoring sources, project_path, and worktree_paths' end - context 'and has multiple sources' do - let(:cobertura) do - <<-EOF.strip_heredoc + context 'and has multiple sources with a pattern for Go projects' do + let(:project_path) { 'local/go' } # Make sure we're not making false positives + let(:sources_xml) do + <<~EOF <sources> - <source>project/src/foo</source> - <source>project/src/bar</source> + <source>/usr/local/go/src</source> + <source>/go/src</source> </sources> EOF end - it_behaves_like 'ignoring sources' + it_behaves_like 'ignoring sources, project_path, and worktree_paths' end - end - context 'when there is a single <class>' do - context 'with no lines' do - let(:cobertura) do - <<-EOF.strip_heredoc - <classes><class filename="app.rb"></class></classes> + context 'and has a single source but already is at the project root path' do + let(:sources_xml) do + <<~EOF + <sources> + <source>builds/#{project_path}</source> + </sources> EOF end - it 'parses XML and returns empty coverage' do - expect { subject }.not_to raise_error + it_behaves_like 'ignoring sources, project_path, and worktree_paths' + end - expect(coverage_report.files).to eq({}) + context 'and has multiple sources but already are at the project root path' do + let(:sources_xml) do + <<~EOF + <sources> + <source>builds/#{project_path}/</source> + <source>builds/somewhere/#{project_path}</source> + </sources> + EOF end + + it_behaves_like 'ignoring sources, project_path, and worktree_paths' end - context 'with a single line' do - let(:cobertura) do - <<-EOF.strip_heredoc - <classes> - <class filename="app.rb"><lines> - <line number="1" hits="2"/> - </lines></class> - </classes> + context 'and has a single source that is not at the project root path' do + let(:sources_xml) do + <<~EOF + <sources> + <source>builds/#{project_path}/app</source> + </sources> EOF end - it 'parses XML and returns a single file with coverage' do - expect { subject }.not_to raise_error + context 'when there is no <class>' do + let(:classes_xml) { '' } - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) - end - end + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error - context 'with multipe lines and methods info' do - let(:cobertura) do - <<-EOF.strip_heredoc - <classes> - <class filename="app.rb"><methods/><lines> - <line number="1" hits="2"/> - <line number="2" hits="0"/> - </lines></class> - </classes> - EOF + expect(coverage_report.files).to eq({}) + end end - it 'parses XML and returns a single file with coverage' do - expect { subject }.not_to raise_error + context 'when there is a single <class>' do + context 'with no lines' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'with a single line but the filename cannot be determined based on extracted source and worktree paths' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="member.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'with a single line' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2 } }) + end + end + + context 'with multiple lines and methods info' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) + end + end + end - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) + context 'when there are multiple <class>' do + context 'with the same filename but the filename cannot be determined based on extracted source and worktree paths' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="member.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="member.rb"><methods/><lines> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end + end + + context 'without a parent package' do + let(:classes_xml) do + <<~EOF + <packages> + <class filename="user.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="user.rb"><methods/><lines> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </packages> + EOF + end + + it 'parses XML and returns coverage information with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) + end + end + + context 'with the same filename and different lines' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="user.rb"><methods/><lines> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with merged coverage, and with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) + end + end + + context 'with the same filename and lines' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="user.rb"><methods/><lines> + <line number="1" hits="1"/> + <line number="2" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with summed-up coverage, and with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 3, 2 => 1 } }) + end + end + + context 'with missing filename' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class><methods/><lines> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and ignores class with missing name' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) + end + end + + context 'with filename that cannot be determined based on extracted source and worktree paths' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="member.rb"><methods/><lines> + <line number="6" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and ignores class with undetermined filename' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) + end + end + + context 'with invalid line information' do + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><methods/><lines> + <line number="1" hits="2"/> + <line number="2" hits="0"/> + </lines></class> + <class filename="user.rb"><methods/><lines> + <line null="test" hits="1"/> + <line number="7" hits="1"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'raises an error' do + expect { parse_report }.to raise_error(described_class::InvalidLineInformationError) + end + end end end - end - context 'when there are multipe <class>' do - context 'with the same filename and different lines' do - let(:cobertura) do - <<-EOF.strip_heredoc - <classes> - <class filename="app.rb"><methods/><lines> - <line number="1" hits="2"/> - <line number="2" hits="0"/> - </lines></class> - <class filename="app.rb"><methods/><lines> - <line number="6" hits="1"/> - <line number="7" hits="1"/> - </lines></class> - </classes> + context 'and has multiple sources that are not at the project root path' do + let(:sources_xml) do + <<~EOF + <sources> + <source>builds/#{project_path}/app1/</source> + <source>builds/#{project_path}/app2/</source> + </sources> EOF end - it 'parses XML and returns a single file with merged coverage' do - expect { subject }.not_to raise_error - - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) + context 'and a class filename is available under multiple extracted sources' do + let(:paths) { ['app1/user.rb', 'app2/user.rb'] } + + let(:classes_xml) do + <<~EOF + <package name="app1"> + <classes> + <class filename="user.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </classes> + </package> + <package name="app2"> + <classes> + <class filename="user.rb"><lines> + <line number="2" hits="3"/> + </lines></class> + </classes> + </package> + EOF + end + + it 'parses XML and returns the files with the filename relative to project root' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ + 'app1/user.rb' => { 1 => 2 }, + 'app2/user.rb' => { 2 => 3 } + }) + end end - end - context 'with the same filename and lines' do - let(:cobertura) do - <<-EOF.strip_heredoc - <packages><package><classes> - <class filename="app.rb"><methods/><lines> - <line number="1" hits="2"/> - <line number="2" hits="0"/> - </lines></class> - <class filename="app.rb"><methods/><lines> - <line number="1" hits="1"/> - <line number="2" hits="1"/> - </lines></class> - </classes></package></packages> - EOF + context 'and a class filename is available under one of the extracted sources' do + let(:paths) { ['app1/member.rb', 'app2/user.rb', 'app2/pet.rb'] } + + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns a single file with the filename relative to project root using the extracted source where it is first found under' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({ 'app2/user.rb' => { 1 => 2 } }) + end end - it 'parses XML and returns a single file with summed-up coverage' do - expect { subject }.not_to raise_error + context 'and a class filename is not found under any of the extracted sources' do + let(:paths) { ['app1/member.rb', 'app2/pet.rb'] } - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } }) + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </classes></package></packages> + EOF + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end end - end - context 'with missing filename' do - let(:cobertura) do - <<-EOF.strip_heredoc - <classes> - <class filename="app.rb"><methods/><lines> - <line number="1" hits="2"/> - <line number="2" hits="0"/> - </lines></class> - <class><methods/><lines> - <line number="6" hits="1"/> - <line number="7" hits="1"/> - </lines></class> - </classes> - EOF + context 'and a class filename is not found under any of the extracted sources within the iteratable limit' do + let(:paths) { ['app2/user.rb'] } + + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="record.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + <class filename="user.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </classes></package></packages> + EOF + end + + before do + stub_const("#{described_class}::MAX_SOURCES", 1) + end + + it 'parses XML and returns empty coverage' do + expect { parse_report }.not_to raise_error + + expect(coverage_report.files).to eq({}) + end end + end + end - it 'parses XML and ignores class with missing name' do - expect { subject }.not_to raise_error + shared_examples_for 'non-smart parsing' do + let(:sources_xml) do + <<~EOF + <sources> + <source>builds/foo/bar/app</source> + </sources> + EOF + end - expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) - end + let(:classes_xml) do + <<~EOF + <packages><package name="app"><classes> + <class filename="user.rb"><lines> + <line number="1" hits="2"/> + </lines></class> + </classes></package></packages> + EOF end - context 'with invalid line information' do - let(:cobertura) do - <<-EOF.strip_heredoc - <classes> - <class filename="app.rb"><methods/><lines> - <line number="1" hits="2"/> - <line number="2" hits="0"/> - </lines></class> - <class filename="app.rb"><methods/><lines> - <line null="test" hits="1"/> - <line number="7" hits="1"/> - </lines></class> - </classes> - EOF - end + it 'parses XML and returns filenames unchanged just as how they are found in the class node' do + expect { parse_report }.not_to raise_error - it 'raises an error' do - expect { subject }.to raise_error(described_class::CoberturaParserError) - end + expect(coverage_report.files).to eq({ 'user.rb' => { 1 => 2 } }) end end + + context 'when project_path is not present' do + let(:project_path) { nil } + let(:paths) { ['app/user.rb'] } + + it_behaves_like 'non-smart parsing' + end + + context 'when worktree_paths is not present' do + let(:project_path) { 'foo/bar' } + let(:paths) { nil } + + it_behaves_like 'non-smart parsing' + end end context 'when data is not Cobertura style XML' do let(:cobertura) { { coverage: '12%' }.to_json } it 'raises an error' do - expect { subject }.to raise_error(described_class::CoberturaParserError) + expect { parse_report }.to raise_error(described_class::InvalidXMLError) end end end diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb index db9a5775d9f..b932cd81272 100644 --- a/spec/lib/gitlab/ci/parsers_spec.rb +++ b/spec/lib/gitlab/ci/parsers_spec.rb @@ -30,6 +30,14 @@ RSpec.describe Gitlab::Ci::Parsers do end end + context 'when file_type is codequality' do + let(:file_type) { 'codequality' } + + it 'fabricates the class' do + is_expected.to be_a(described_class::Codequality::CodeClimate) + end + end + context 'when file_type is terraform' do let(:file_type) { 'terraform' } diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb new file mode 100644 index 00000000000..78363be7f36 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project, reload: true) { create(:project, namespace: namespace) } + let_it_be(:plan_limits, reload: true) { create(:plan_limits, :default_plan) } + + let(:pipeline_seed) { double(:pipeline_seed, deployments_count: 2) } + let(:save_incompleted) { false } + + let(:command) do + double(:command, + project: project, + pipeline_seed: pipeline_seed, + save_incompleted: save_incompleted + ) + end + + let(:pipeline) { build(:ci_pipeline, project: project) } + let(:step) { described_class.new(pipeline, command) } + + subject(:perform) { step.perform! } + + context 'when pipeline deployments limit is exceeded' do + before do + plan_limits.update!(ci_pipeline_deployments: 1) + end + + context 'when saving incompleted pipelines' do + let(:save_incompleted) { true } + + it 'drops the pipeline' do + perform + + expect(pipeline).to be_persisted + expect(pipeline.reload).to be_failed + end + + it 'breaks the chain' do + perform + + expect(step.break?).to be true + end + + it 'sets a valid failure reason' do + perform + + expect(pipeline.deployments_limit_exceeded?).to be true + end + end + + context 'when not saving incomplete pipelines' do + let(:save_incompleted) { false } + + it 'does not persist the pipeline' do + perform + + expect(pipeline).not_to be_persisted + end + + it 'breaks the chain' do + perform + + expect(step.break?).to be true + end + + it 'adds an informative error to the pipeline' do + perform + + expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.']) + end + end + + it 'logs the error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(Gitlab::Ci::Limit::LimitExceededError), + project_id: project.id, plan: namespace.actual_plan_name + ) + + perform + end + end + + context 'when pipeline deployments limit is not exceeded' do + before do + plan_limits.update!(ci_pipeline_deployments: 100) + end + + it 'does not break the chain' do + perform + + expect(step.break?).to be false + end + + it 'does not invalidate the pipeline' do + perform + + expect(pipeline.errors).to be_empty + end + + it 'does not log any error' do + expect(Gitlab::ErrorTracking).not_to receive(:track_exception) + + perform + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb index d849c768a3c..0ce8b80902e 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb @@ -50,8 +50,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do it 'sets the seeds in the command object' do run_chain - expect(command.stage_seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Base) - expect(command.stage_seeds.count).to eq 1 + expect(command.pipeline_seed).to be_a(Gitlab::Ci::Pipeline::Seed::Pipeline) + expect(command.pipeline_seed.size).to eq 1 end context 'when no ref policy is specified' do @@ -63,16 +63,18 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do } end - it 'correctly fabricates a stage seeds object' do + it 'correctly fabricates stages and builds' do run_chain - seeds = command.stage_seeds - expect(seeds.size).to eq 2 - expect(seeds.first.attributes[:name]).to eq 'test' - expect(seeds.second.attributes[:name]).to eq 'deploy' - expect(seeds.dig(0, 0, :name)).to eq 'rspec' - expect(seeds.dig(0, 1, :name)).to eq 'spinach' - expect(seeds.dig(1, 0, :name)).to eq 'production' + seed = command.pipeline_seed + + expect(seed.stages.size).to eq 2 + expect(seed.size).to eq 3 + expect(seed.stages.first.name).to eq 'test' + expect(seed.stages.second.name).to eq 'deploy' + expect(seed.stages[0].statuses[0].name).to eq 'rspec' + expect(seed.stages[0].statuses[1].name).to eq 'spinach' + expect(seed.stages[1].statuses[0].name).to eq 'production' end end @@ -88,14 +90,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do } end - it 'returns stage seeds only assigned to master' do + it 'returns pipeline seed with jobs only assigned to master' do run_chain - seeds = command.stage_seeds + seed = command.pipeline_seed - expect(seeds.size).to eq 1 - expect(seeds.first.attributes[:name]).to eq 'test' - expect(seeds.dig(0, 0, :name)).to eq 'spinach' + expect(seed.size).to eq 1 + expect(seed.stages.first.name).to eq 'test' + expect(seed.stages[0].statuses[0].name).to eq 'spinach' end end @@ -109,14 +111,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do } end - it 'returns stage seeds only assigned to schedules' do + it 'returns pipeline seed with jobs only assigned to schedules' do run_chain - seeds = command.stage_seeds + seed = command.pipeline_seed - expect(seeds.size).to eq 1 - expect(seeds.first.attributes[:name]).to eq 'test' - expect(seeds.dig(0, 0, :name)).to eq 'spinach' + expect(seed.size).to eq 1 + expect(seed.stages.first.name).to eq 'test' + expect(seed.stages[0].statuses[0].name).to eq 'spinach' end end @@ -141,11 +143,11 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do it 'returns seeds for kubernetes dependent job' do run_chain - seeds = command.stage_seeds + seed = command.pipeline_seed - expect(seeds.size).to eq 2 - expect(seeds.dig(0, 0, :name)).to eq 'spinach' - expect(seeds.dig(1, 0, :name)).to eq 'production' + expect(seed.size).to eq 2 + expect(seed.stages[0].statuses[0].name).to eq 'spinach' + expect(seed.stages[1].statuses[0].name).to eq 'production' end end end @@ -154,10 +156,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do it 'does not return seeds for kubernetes dependent job' do run_chain - seeds = command.stage_seeds + seed = command.pipeline_seed - expect(seeds.size).to eq 1 - expect(seeds.dig(0, 0, :name)).to eq 'spinach' + expect(seed.size).to eq 1 + expect(seed.stages[0].statuses[0].name).to eq 'spinach' end end end @@ -173,10 +175,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do it 'returns stage seeds only when variables expression is truthy' do run_chain - seeds = command.stage_seeds + seed = command.pipeline_seed - expect(seeds.size).to eq 1 - expect(seeds.dig(0, 0, :name)).to eq 'unit' + expect(seed.size).to eq 1 + expect(seed.stages[0].statuses[0].name).to eq 'unit' end end diff --git a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb new file mode 100644 index 00000000000..c52994fc6a2 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:default_plan, reload: true) { create(:default_plan) } + let_it_be(:project, reload: true) { create(:project, :repository, namespace: namespace) } + let_it_be(:plan_limits) { create(:plan_limits, plan: default_plan) } + + let(:pipeline) { build_stubbed(:ci_pipeline, project: project) } + + let(:pipeline_seed) { double(:pipeline_seed, deployments_count: 2)} + + let(:command) do + double(:command, + project: project, + pipeline_seed: pipeline_seed, + save_incompleted: true + ) + end + + let(:ci_pipeline_deployments_limit) { 0 } + + before do + plan_limits.update!(ci_pipeline_deployments: ci_pipeline_deployments_limit) + end + + subject(:quota) { described_class.new(namespace, pipeline, command) } + + shared_context 'limit exceeded' do + let(:ci_pipeline_deployments_limit) { 1 } + end + + shared_context 'limit not exceeded' do + let(:ci_pipeline_deployments_limit) { 2 } + end + + describe '#enabled?' do + context 'when limit is enabled in plan' do + let(:ci_pipeline_deployments_limit) { 10 } + + it 'is enabled' do + expect(quota).to be_enabled + end + end + + context 'when limit is not enabled' do + let(:ci_pipeline_deployments_limit) { 0 } + + it 'is not enabled' do + expect(quota).not_to be_enabled + end + end + + context 'when limit does not exist' do + before do + allow(namespace).to receive(:actual_plan) { create(:default_plan) } + end + + it 'is enabled by default' do + expect(quota).to be_enabled + end + end + end + + describe '#exceeded?' do + context 'when limit is exceeded' do + include_context 'limit exceeded' + + it 'is exceeded' do + expect(quota).to be_exceeded + end + end + + context 'when limit is not exceeded' do + include_context 'limit not exceeded' + + it 'is not exceeded' do + expect(quota).not_to be_exceeded + end + end + end + + describe '#message' do + context 'when limit is exceeded' do + include_context 'limit exceeded' + + it 'returns info about pipeline deployment limit exceeded' do + expect(quota.message) + .to eq "Pipeline has too many deployments! Requested 2, but the limit is 1." + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 0b961336f3f..bc10e94c81d 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -71,6 +71,33 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do end end + context 'with job:rules:[variables:]' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }, + { key: 'VAR2', value: 'var 2', public: true }], + rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } + end + + it do + is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1', public: true }, + { key: 'VAR2', value: 'var 2', public: true }, + { key: 'VAR3', value: 'var 3', public: true }]) + end + + context 'when FF ci_rules_variables is disabled' do + before do + stub_feature_flags(ci_rules_variables: false) + end + + it do + is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'var 1', public: true }, + { key: 'VAR2', value: 'var 2', public: true }]) + end + end + end + context 'with cache:key' do let(:attributes) do { @@ -165,6 +192,45 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do it { is_expected.to include(options: {}) } end + + context 'with allow_failure' do + let(:options) do + { allow_failure_criteria: { exit_codes: [42] } } + end + + let(:rules) do + [{ if: '$VAR == null', when: 'always' }] + end + + let(:attributes) do + { + name: 'rspec', + ref: 'master', + options: options, + rules: rules + } + end + + context 'when rules does not override allow_failure' do + it { is_expected.to match a_hash_including(options: options) } + end + + context 'when rules set allow_failure to true' do + let(:rules) do + [{ if: '$VAR == null', when: 'always', allow_failure: true }] + end + + it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } + end + + context 'when rules set allow_failure to false' do + let(:rules) do + [{ if: '$VAR == null', when: 'always', allow_failure: false }] + end + + it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } + end + end end describe '#bridge?' do diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb index e62bf042fba..664aaaedf7b 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb @@ -85,16 +85,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do end it_behaves_like 'returning a correct environment' - - context 'but the environment auto_stop_in on create flag is disabled' do - let(:expected_auto_stop_in) { nil } - - before do - stub_feature_flags(environment_auto_stop_start_on_create: false) - end - - it_behaves_like 'returning a correct environment' - end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb new file mode 100644 index 00000000000..1790388da03 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/seed/pipeline_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Seed::Pipeline do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let(:stages_attributes) do + [ + { + name: 'build', + index: 0, + builds: [ + { name: 'init', scheduling_type: :stage }, + { name: 'build', scheduling_type: :stage } + ] + }, + { + name: 'test', + index: 1, + builds: [ + { name: 'rspec', scheduling_type: :stage }, + { name: 'staging', scheduling_type: :stage, environment: 'staging' }, + { name: 'deploy', scheduling_type: :stage, environment: 'production' } + ] + } + ] + end + + subject(:seed) do + described_class.new(pipeline, stages_attributes) + end + + describe '#stages' do + it 'returns the stage resources' do + stages = seed.stages + + expect(stages).to all(be_a(Ci::Stage)) + expect(stages.map(&:name)).to contain_exactly('build', 'test') + end + end + + describe '#size' do + it 'returns the number of jobs' do + expect(seed.size).to eq(5) + end + end + + describe '#errors' do + context 'when attributes are valid' do + it 'returns nil' do + expect(seed.errors).to be_nil + end + end + + context 'when attributes are not valid' do + it 'returns the errors' do + stages_attributes[0][:builds] << { + name: 'invalid_job', + scheduling_type: :dag, + needs_attributes: [{ name: 'non-existent', artifacts: true }] + } + + expect(seed.errors).to contain_exactly("invalid_job: needs 'non-existent'") + end + end + end + + describe '#deployments_count' do + it 'counts the jobs having an environment associated' do + expect(seed.deployments_count).to eq(2) + end + end +end diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb index 650ae41320b..ade0e36cf1e 100644 --- a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do - let(:comparer) { described_class.new(base_reports, head_reports) } - let(:base_reports) { Gitlab::Ci::Reports::AccessibilityReports.new } - let(:head_reports) { Gitlab::Ci::Reports::AccessibilityReports.new } + let(:comparer) { described_class.new(base_report, head_report) } + let(:base_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + let(:head_report) { Gitlab::Ci::Reports::AccessibilityReports.new } let(:url) { "https://gitlab.com" } let(:single_error) do [ @@ -38,233 +38,254 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReportsComparer do end describe '#status' do - subject { comparer.status } + subject(:status) { comparer.status } context 'when head report has an error' do before do - head_reports.add_url(url, single_error) + head_report.add_url(url, single_error) end it 'returns status failed' do - expect(subject).to eq(described_class::STATUS_FAILED) + expect(status).to eq(described_class::STATUS_FAILED) end end context 'when head reports does not have errors' do before do - head_reports.add_url(url, []) + head_report.add_url(url, []) end it 'returns status success' do - expect(subject).to eq(described_class::STATUS_SUCCESS) + expect(status).to eq(described_class::STATUS_SUCCESS) end end end describe '#errors_count' do - subject { comparer.errors_count } + subject(:errors_count) { comparer.errors_count } context 'when head report has an error' do before do - head_reports.add_url(url, single_error) + head_report.add_url(url, single_error) end it 'returns the number of new errors' do - expect(subject).to eq(1) + expect(errors_count).to eq(1) end end context 'when head reports does not have an error' do before do - head_reports.add_url(url, []) + head_report.add_url(url, []) end it 'returns the number new errors' do - expect(subject).to eq(0) + expect(errors_count).to eq(0) end end end describe '#resolved_count' do - subject { comparer.resolved_count } + subject(:resolved_count) { comparer.resolved_count } context 'when base reports has an error and head has a different error' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, different_error) + base_report.add_url(url, single_error) + head_report.add_url(url, different_error) end it 'returns the resolved count' do - expect(subject).to eq(1) + expect(resolved_count).to eq(1) end end context 'when base reports has errors head has no errors' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, []) + base_report.add_url(url, single_error) + head_report.add_url(url, []) end it 'returns the resolved count' do - expect(subject).to eq(1) + expect(resolved_count).to eq(1) end end context 'when base reports has errors and head has the same error' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, single_error) + base_report.add_url(url, single_error) + head_report.add_url(url, single_error) end it 'returns zero' do - expect(subject).to eq(0) + expect(resolved_count).to eq(0) end end context 'when base reports does not have errors and head has errors' do before do - head_reports.add_url(url, single_error) + head_report.add_url(url, single_error) end it 'returns the number of resolved errors' do - expect(subject).to eq(0) + expect(resolved_count).to eq(0) end end end describe '#total_count' do - subject { comparer.total_count } + subject(:total_count) { comparer.total_count } context 'when base reports has an error' do before do - base_reports.add_url(url, single_error) + base_report.add_url(url, single_error) end - it 'returns the error count' do - expect(subject).to eq(1) + it 'returns zero' do + expect(total_count).to be_zero end end context 'when head report has an error' do before do - head_reports.add_url(url, single_error) + head_report.add_url(url, single_error) end - it 'returns the error count' do - expect(subject).to eq(1) + it 'returns the total count' do + expect(total_count).to eq(1) end end context 'when base report has errors and head report has errors' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, different_error) + base_report.add_url(url, single_error) + head_report.add_url(url, different_error) + end + + it 'returns the total count' do + expect(total_count).to eq(1) + end + end + + context 'when base report has errors and head report has the same error' do + before do + base_report.add_url(url, single_error) + head_report.add_url(url, single_error + different_error) end - it 'returns the error count' do - expect(subject).to eq(2) + it 'returns the total count' do + expect(total_count).to eq(2) end end end describe '#existing_errors' do - subject { comparer.existing_errors } + subject(:existing_errors) { comparer.existing_errors } context 'when base report has errors and head has a different error' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, different_error) + base_report.add_url(url, single_error) + head_report.add_url(url, different_error) end - it 'returns the existing errors' do - expect(subject.size).to eq(1) - expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") + it 'returns an empty array' do + expect(existing_errors).to be_empty end end context 'when base report does not have errors and head has errors' do before do - base_reports.add_url(url, []) - head_reports.add_url(url, single_error) + base_report.add_url(url, []) + head_report.add_url(url, single_error) end it 'returns an empty array' do - expect(subject).to be_empty + expect(existing_errors).to be_empty + end + end + + context 'when base report has errors and head report has the same error' do + before do + base_report.add_url(url, single_error) + head_report.add_url(url, single_error + different_error) + end + + it 'returns the existing error' do + expect(existing_errors).to eq(single_error) end end end describe '#new_errors' do - subject { comparer.new_errors } + subject(:new_errors) { comparer.new_errors } context 'when base reports has errors and head has more errors' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, single_error + different_error) + base_report.add_url(url, single_error) + head_report.add_url(url, single_error + different_error) end it 'returns new errors between base and head reports' do - expect(subject.size).to eq(1) - expect(subject.first["code"]).to eq("WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail") + expect(new_errors.size).to eq(1) + expect(new_errors.first["code"]).to eq("WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail") end end context 'when base reports has an error and head has no errors' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, []) + base_report.add_url(url, single_error) + head_report.add_url(url, []) end it 'returns an empty array' do - expect(subject).to be_empty + expect(new_errors).to be_empty end end context 'when base reports does not have errors and head has errors' do before do - head_reports.add_url(url, single_error) + head_report.add_url(url, single_error) end it 'returns the new error' do - expect(subject.size).to eq(1) - expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") + expect(new_errors.size).to eq(1) + expect(new_errors.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") end end end describe '#resolved_errors' do - subject { comparer.resolved_errors } + subject(:resolved_errors) { comparer.resolved_errors } context 'when base report has errors and head has more errors' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, single_error + different_error) + base_report.add_url(url, single_error) + head_report.add_url(url, single_error + different_error) end it 'returns an empty array' do - expect(subject).to be_empty + expect(resolved_errors).to be_empty end end context 'when base reports has errors and head has a different error' do before do - base_reports.add_url(url, single_error) - head_reports.add_url(url, different_error) + base_report.add_url(url, single_error) + head_report.add_url(url, different_error) end it 'returns the resolved errors' do - expect(subject.size).to eq(1) - expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") + expect(resolved_errors.size).to eq(1) + expect(resolved_errors.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") end end context 'when base reports does not have errors and head has errors' do before do - head_reports.add_url(url, single_error) + head_report.add_url(url, single_error) end it 'returns an empty array' do - expect(subject).to be_empty + expect(resolved_errors).to be_empty end end end diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb new file mode 100644 index 00000000000..7053d54381b --- /dev/null +++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb @@ -0,0 +1,308 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do + let(:comparer) { described_class.new(base_report, head_report) } + let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new } + let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new } + let(:degradation_1) do + { + "categories": [ + "Complexity" + ], + "check_name": "argument_count", + "content": { + "body": "" + }, + "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", + "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", + "location": { + "path": "foo.rb", + "lines": { + "begin": 10, + "end": 10 + } + }, + "other_locations": [], + "remediation_points": 900000, + "severity": "major", + "type": "issue", + "engine_name": "structure" + }.with_indifferent_access + end + + let(:degradation_2) do + { + "type": "Issue", + "check_name": "Rubocop/Metrics/ParameterLists", + "description": "Avoid parameter lists longer than 5 parameters. [12/5]", + "categories": [ + "Complexity" + ], + "remediation_points": 550000, + "location": { + "path": "foo.rb", + "positions": { + "begin": { + "column": 14, + "line": 10 + }, + "end": { + "column": 39, + "line": 10 + } + } + }, + "content": { + "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count." + }, + "engine_name": "rubocop", + "fingerprint": "ab5f8b935886b942d621399f5a2ca16e", + "severity": "minor" + }.with_indifferent_access + end + + describe '#status' do + subject(:report_status) { comparer.status } + + context 'when head report has an error' do + before do + head_report.add_degradation(degradation_1) + end + + it 'returns status failed' do + expect(report_status).to eq(described_class::STATUS_FAILED) + end + end + + context 'when head report does not have errors' do + it 'returns status success' do + expect(report_status).to eq(described_class::STATUS_SUCCESS) + end + end + end + + describe '#errors_count' do + subject(:errors_count) { comparer.errors_count } + + context 'when head report has an error' do + before do + head_report.add_degradation(degradation_1) + end + + it 'returns the number of new errors' do + expect(errors_count).to eq(1) + end + end + + context 'when head report does not have an error' do + it 'returns zero' do + expect(errors_count).to be_zero + end + end + end + + describe '#resolved_count' do + subject(:resolved_count) { comparer.resolved_count } + + context 'when base report has an error and head has a different error' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_2) + end + + it 'counts the base report error as resolved' do + expect(resolved_count).to eq(1) + end + end + + context 'when base report has errors head has no errors' do + before do + base_report.add_degradation(degradation_1) + end + + it 'counts the base report errors as resolved' do + expect(resolved_count).to eq(1) + end + end + + context 'when base report has errors and head has the same error' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_1) + end + + it 'returns zero' do + expect(resolved_count).to eq(0) + end + end + + context 'when base report does not have errors and head has errors' do + before do + head_report.add_degradation(degradation_1) + end + + it 'returns zero' do + expect(resolved_count).to be_zero + end + end + end + + describe '#total_count' do + subject(:total_count) { comparer.total_count } + + context 'when base report has an error' do + before do + base_report.add_degradation(degradation_1) + end + + it 'returns zero' do + expect(total_count).to be_zero + end + end + + context 'when head report has an error' do + before do + head_report.add_degradation(degradation_1) + end + + it 'includes the head report error in the count' do + expect(total_count).to eq(1) + end + end + + context 'when base report has errors and head report has errors' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_2) + end + + it 'includes errors in the count' do + expect(total_count).to eq(1) + end + end + + context 'when base report has errors and head report has the same error' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_2) + end + + it 'includes errors in the count' do + expect(total_count).to eq(2) + end + end + end + + describe '#existing_errors' do + subject(:existing_errors) { comparer.existing_errors } + + context 'when base report has errors and head has the same error' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_2) + end + + it 'includes the base report errors' do + expect(existing_errors).to contain_exactly(degradation_1) + end + end + + context 'when base report has errors and head has a different error' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_2) + end + + it 'returns an empty array' do + expect(existing_errors).to be_empty + end + end + + context 'when base report does not have errors and head has errors' do + before do + head_report.add_degradation(degradation_1) + end + + it 'returns an empty array' do + expect(existing_errors).to be_empty + end + end + end + + describe '#new_errors' do + subject(:new_errors) { comparer.new_errors } + + context 'when base report has errors and head has more errors' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_2) + end + + it 'includes errors not found in the base report' do + expect(new_errors).to eq([degradation_2]) + end + end + + context 'when base report has an error and head has no errors' do + before do + base_report.add_degradation(degradation_1) + end + + it 'returns an empty array' do + expect(new_errors).to be_empty + end + end + + context 'when base report does not have errors and head has errors' do + before do + head_report.add_degradation(degradation_1) + end + + it 'returns the head report error' do + expect(new_errors).to eq([degradation_1]) + end + end + end + + describe '#resolved_errors' do + subject(:resolved_errors) { comparer.resolved_errors } + + context 'when base report errors are still found in the head report' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_2) + end + + it 'returns an empty array' do + expect(resolved_errors).to be_empty + end + end + + context 'when base report has errors and head has a different error' do + before do + base_report.add_degradation(degradation_1) + head_report.add_degradation(degradation_2) + end + + it 'returns the base report error' do + expect(resolved_errors).to eq([degradation_1]) + end + end + + context 'when base report does not have errors and head has errors' do + before do + head_report.add_degradation(degradation_1) + end + + it 'returns an empty array' do + expect(resolved_errors).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb new file mode 100644 index 00000000000..44e67259369 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::CodequalityReports do + let(:codequality_report) { described_class.new } + let(:degradation_1) do + { + "categories": [ + "Complexity" + ], + "check_name": "argument_count", + "content": { + "body": "" + }, + "description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.", + "fingerprint": "15cdb5c53afd42bc22f8ca366a08d547", + "location": { + "path": "foo.rb", + "lines": { + "begin": 10, + "end": 10 + } + }, + "other_locations": [], + "remediation_points": 900000, + "severity": "major", + "type": "issue", + "engine_name": "structure" + }.with_indifferent_access + end + + let(:degradation_2) do + { + "type": "Issue", + "check_name": "Rubocop/Metrics/ParameterLists", + "description": "Avoid parameter lists longer than 5 parameters. [12/5]", + "categories": [ + "Complexity" + ], + "remediation_points": 550000, + "location": { + "path": "foo.rb", + "positions": { + "begin": { + "column": 14, + "line": 10 + }, + "end": { + "column": 39, + "line": 10 + } + } + }, + "content": { + "body": "This cop checks for methods with too many parameters.\nThe maximum number of parameters is configurable.\nKeyword arguments can optionally be excluded from the total count." + }, + "engine_name": "rubocop", + "fingerprint": "ab5f8b935886b942d621399f5a2ca16e", + "severity": "minor" + }.with_indifferent_access + end + + it { expect(codequality_report.degradations).to eq({}) } + + describe '#add_degradation' do + context 'when there is a degradation' do + before do + codequality_report.add_degradation(degradation_1) + end + + it 'adds degradation to codequality report' do + expect(codequality_report.degradations.keys).to eq([degradation_1[:fingerprint]]) + expect(codequality_report.degradations.values.size).to eq(1) + end + end + + context 'when a required property is missing in the degradation' do + let(:invalid_degradation) do + { + "type": "Issue", + "check_name": "Rubocop/Metrics/ParameterLists", + "description": "Avoid parameter lists longer than 5 parameters. [12/5]", + "fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf", + "severity": "minor" + }.with_indifferent_access + end + + it 'sets location as an error' do + codequality_report.add_degradation(invalid_degradation) + + expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'") + end + end + end + + describe '#set_error_message' do + context 'when there is an error' do + it 'sets errors' do + codequality_report.set_error_message("error") + + expect(codequality_report.error_message).to eq("error") + end + end + end + + describe '#degradations_count' do + subject(:degradations_count) { codequality_report.degradations_count } + + context 'when there are many degradations' do + before do + codequality_report.add_degradation(degradation_1) + codequality_report.add_degradation(degradation_2) + end + + it 'returns the number of degradations' do + expect(degradations_count).to eq(2) + end + end + end + + describe '#all_degradations' do + subject(:all_degradations) { codequality_report.all_degradations } + + context 'when there are many degradations' do + before do + codequality_report.add_degradation(degradation_1) + codequality_report.add_degradation(degradation_2) + end + + it 'returns all degradations' do + expect(all_degradations).to contain_exactly(degradation_1, degradation_2) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb new file mode 100644 index 00000000000..1e5e4766583 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/reports_comparer_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::ReportsComparer do + let(:comparer) { described_class.new(base_report, head_report) } + let(:base_report) { Gitlab::Ci::Reports::CodequalityReports.new } + let(:head_report) { Gitlab::Ci::Reports::CodequalityReports.new } + + describe '#initialize' do + context 'sets getter for the report comparer' do + it 'return base report' do + expect(comparer.base_report).to be_an_instance_of(Gitlab::Ci::Reports::CodequalityReports) + end + + it 'return head report' do + expect(comparer.head_report).to be_an_instance_of(Gitlab::Ci::Reports::CodequalityReports) + end + end + end + + describe '#status' do + subject(:status) { comparer.status } + + it 'returns not implemented error' do + expect { status }.to raise_error(NotImplementedError) + end + + context 'when success? is true' do + before do + allow(comparer).to receive(:success?).and_return(true) + end + + it 'returns status success' do + expect(status).to eq('success') + end + end + + context 'when success? is false' do + before do + allow(comparer).to receive(:success?).and_return(false) + end + + it 'returns status failed' do + expect(status).to eq('failed') + end + end + end + + describe '#success?' do + subject(:success?) { comparer.success? } + + it 'returns not implemented error' do + expect { success? }.to raise_error(NotImplementedError) + end + end + + describe '#existing_errors' do + subject(:existing_errors) { comparer.existing_errors } + + it 'returns not implemented error' do + expect { existing_errors }.to raise_error(NotImplementedError) + end + end + + describe '#resolved_errors' do + subject(:resolved_errors) { comparer.resolved_errors } + + it 'returns not implemented error' do + expect { resolved_errors }.to raise_error(NotImplementedError) + end + end + + describe '#errors_count' do + subject(:errors_count) { comparer.errors_count } + + it 'returns not implemented error' do + expect { errors_count }.to raise_error(NotImplementedError) + end + end + + describe '#resolved_count' do + subject(:resolved_count) { comparer.resolved_count } + + it 'returns not implemented error' do + expect { resolved_count }.to raise_error(NotImplementedError) + end + end + + describe '#total_count' do + subject(:total_count) { comparer.total_count } + + it 'returns not implemented error' do + expect { total_count }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/lib/gitlab/ci/templates/npm_spec.rb b/spec/lib/gitlab/ci/templates/npm_spec.rb new file mode 100644 index 00000000000..1f8e32ce019 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/npm_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'npm.latest.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('npm.latest') } + + describe 'the created pipeline' do + let_it_be(:user) { create(:admin) } + + let(:repo_files) { { 'package.json' => '{}', 'README.md' => '' } } + let(:modified_files) { %w[package.json] } + let(:project) { create(:project, :custom_repo, files: repo_files) } + let(:pipeline_branch) { project.default_branch } + let(:pipeline_tag) { 'v1.2.1' } + let(:pipeline_ref) { pipeline_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref ) } + let(:pipeline) { service.execute!(:push) } + let(:build_names) { pipeline.builds.pluck(:name) } + + def create_branch(name:) + ::Branches::CreateService.new(project, user).execute(name, project.default_branch) + end + + def create_tag(name:) + ::Tags::CreateService.new(project, user).execute(name, project.default_branch, nil) + end + + before do + stub_ci_pipeline_yaml_file(template.content) + + create_branch(name: pipeline_branch) + create_tag(name: pipeline_tag) + + allow_any_instance_of(Ci::Pipeline).to receive(:modified_paths).and_return(modified_files) + end + + shared_examples 'publish job created' do + it 'creates a pipeline with a single job: publish' do + expect(build_names).to eq(%w[publish]) + end + end + + shared_examples 'no pipeline created' do + it 'does not create a pipeline because the only job (publish) is not created' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError, 'No stages / jobs for this pipeline.') + end + end + + context 'on default branch' do + context 'when package.json has been changed' do + it_behaves_like 'publish job created' + end + + context 'when package.json does not exist or has not been changed' do + let(:modified_files) { %w[README.md] } + + it_behaves_like 'no pipeline created' + end + end + + %w[v1.0.0 v2.1.0-alpha].each do |valid_version| + context "when the branch name is #{valid_version}" do + let(:pipeline_branch) { valid_version } + + it_behaves_like 'publish job created' + end + + context "when the tag name is #{valid_version}" do + let(:pipeline_tag) { valid_version } + let(:pipeline_ref) { pipeline_tag } + + it_behaves_like 'publish job created' + end + end + + %w[patch-1 my-feature-branch v1 v1.0 2.1.0].each do |invalid_version| + context "when the branch name is #{invalid_version}" do + let(:pipeline_branch) { invalid_version } + + it_behaves_like 'no pipeline created' + end + + context "when the tag name is #{invalid_version}" do + let(:pipeline_tag) { invalid_version } + let(:pipeline_ref) { pipeline_tag } + + it_behaves_like 'no pipeline created' + end + end + end +end diff --git a/spec/lib/gitlab/ci/trace/checksum_spec.rb b/spec/lib/gitlab/ci/trace/checksum_spec.rb index 794794c3f69..a343d74f755 100644 --- a/spec/lib/gitlab/ci/trace/checksum_spec.rb +++ b/spec/lib/gitlab/ci/trace/checksum_spec.rb @@ -8,8 +8,12 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do subject { described_class.new(build) } context 'when build pending state exists' do + let(:trace_details) do + { trace_checksum: 'crc32:d4777540', trace_bytesize: 262161 } + end + before do - create(:ci_build_pending_state, build: build, trace_checksum: 'crc32:d4777540') + create(:ci_build_pending_state, build: build, **trace_details) end context 'when matching persisted trace chunks exist' do @@ -22,6 +26,7 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do it 'calculates combined trace chunks CRC32 correctly' do expect(subject.chunks_crc32).to eq 3564598592 expect(subject).to be_valid + expect(subject).not_to be_corrupted end end @@ -32,8 +37,9 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do create_chunk(index: 2, data: 'ccccccccccccccccc') end - it 'makes trace checksum invalid' do + it 'makes trace checksum invalid but not corrupted' do expect(subject).not_to be_valid + expect(subject).not_to be_corrupted end end @@ -43,8 +49,9 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do create_chunk(index: 2, data: 'ccccccccccccccccc') end - it 'makes trace checksum invalid' do + it 'makes trace checksum invalid and corrupted' do expect(subject).not_to be_valid + expect(subject).to be_corrupted end end @@ -55,8 +62,9 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do create_chunk(index: 2, data: 'ccccccccccccccccc') end - it 'makes trace checksum invalid' do + it 'makes trace checksum invalid but not corrupted' do expect(subject).not_to be_valid + expect(subject).not_to be_corrupted end end @@ -99,6 +107,14 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do it 'returns nil' do expect(subject.last_chunk).to be_nil end + + it 'is not a valid trace' do + expect(subject).not_to be_valid + end + + it 'is not a corrupted trace' do + expect(subject).not_to be_corrupted + end end context 'when there are multiple chunks' do @@ -110,6 +126,26 @@ RSpec.describe Gitlab::Ci::Trace::Checksum do it 'returns chunk with the highest index' do expect(subject.last_chunk.chunk_index).to eq 1 end + + it 'is not a valid trace' do + expect(subject).not_to be_valid + end + + it 'is not a corrupted trace' do + expect(subject).not_to be_corrupted + end + end + end + + describe '#trace_size' do + before do + create_chunk(index: 0, data: 'a' * 128.kilobytes) + create_chunk(index: 1, data: 'b' * 128.kilobytes) + create_chunk(index: 2, data: 'abcdefg-ü') + end + + it 'returns total trace size in bytes' do + expect(subject.trace_size).to eq 262154 end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index fb6395e888a..5ad1b3dd241 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -231,6 +231,23 @@ module Gitlab expect(subject[:allow_failure]).to be true end end + + context 'when allow_failure has exit_codes' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual', + allow_failure: { exit_codes: 1 } }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + + it 'saves allow_failure_criteria into options' do + expect(subject[:options]).to match( + a_hash_including(allow_failure_criteria: { exit_codes: [1] })) + end + end end context 'when job is not a manual action' do @@ -254,6 +271,22 @@ module Gitlab expect(subject[:allow_failure]).to be false end end + + context 'when allow_failure is dynamically specified' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + allow_failure: { exit_codes: 1 } }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + + it 'saves allow_failure_criteria into options' do + expect(subject[:options]).to match( + a_hash_including(allow_failure_criteria: { exit_codes: [1] })) + end + end end end @@ -2111,6 +2144,71 @@ module Gitlab end end + describe 'cross pipeline needs' do + context 'when configuration is valid' do + let(:config) do + <<~YAML + rspec: + stage: test + script: rspec + needs: + - pipeline: $THE_PIPELINE_ID + job: dependency-job + YAML + end + + it 'returns a valid configuration and sets artifacts: true by default' do + expect(subject).to be_valid + + rspec = subject.build_attributes(:rspec) + expect(rspec.dig(:options, :cross_dependencies)).to eq( + [{ pipeline: '$THE_PIPELINE_ID', job: 'dependency-job', artifacts: true }] + ) + end + + context 'when pipeline ID is hard-coded' do + let(:config) do + <<~YAML + rspec: + stage: test + script: rspec + needs: + - pipeline: "123" + job: dependency-job + YAML + end + + it 'returns a valid configuration and sets artifacts: true by default' do + expect(subject).to be_valid + + rspec = subject.build_attributes(:rspec) + expect(rspec.dig(:options, :cross_dependencies)).to eq( + [{ pipeline: '123', job: 'dependency-job', artifacts: true }] + ) + end + end + end + + context 'when configuration is not valid' do + let(:config) do + <<~YAML + rspec: + stage: test + script: rspec + needs: + - pipeline: $THE_PIPELINE_ID + job: dependency-job + something: else + YAML + end + + it 'returns an error' do + expect(subject).not_to be_valid + expect(subject.errors).to include(/:need config contains unknown keys: something/) + end + end + end + describe "Hidden jobs" do let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } @@ -2429,7 +2527,13 @@ module Gitlab context 'returns errors if job allow_failure parameter is not an boolean' do let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) } - it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a boolean value' + it_behaves_like 'returns errors', 'jobs:rspec allow failure should be a hash or a boolean value' + end + + context 'returns errors if job exit_code parameter from allow_failure is not an integer' do + let(:config) { YAML.dump({ rspec: { script: "test", allow_failure: { exit_codes: 'string' } } }) } + + it_behaves_like 'returns errors', 'jobs:rspec:allow_failure exit codes should be an array of integers or an integer' end context 'returns errors if job stage is not a string' do diff --git a/spec/lib/gitlab/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb index 05d744d95e2..a99bdcc9a0f 100644 --- a/spec/lib/gitlab/cleanup/project_uploads_spec.rb +++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb @@ -15,10 +15,10 @@ RSpec.describe Gitlab::Cleanup::ProjectUploads do describe '#run!' do shared_examples_for 'moves the file' do shared_examples_for 'a real run' do - let(:args) { [dry_run: false] } + let(:args) { { dry_run: false } } it 'moves the file to its proper location' do - subject.run!(*args) + subject.run!(**args) expect(File.exist?(path)).to be_falsey expect(File.exist?(new_path)).to be_truthy @@ -28,13 +28,13 @@ RSpec.describe Gitlab::Cleanup::ProjectUploads do expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up...") expect(logger).to receive(:info).with("Did #{action}") - subject.run!(*args) + subject.run!(**args) end end shared_examples_for 'a dry run' do it 'does not move the file' do - subject.run!(*args) + subject.run!(**args) expect(File.exist?(path)).to be_truthy expect(File.exist?(new_path)).to be_falsey @@ -44,30 +44,30 @@ RSpec.describe Gitlab::Cleanup::ProjectUploads do expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up. Dry run...") expect(logger).to receive(:info).with("Can #{action}") - subject.run!(*args) + subject.run!(**args) end end context 'when dry_run is false' do - let(:args) { [dry_run: false] } + let(:args) { { dry_run: false } } it_behaves_like 'a real run' end context 'when dry_run is nil' do - let(:args) { [dry_run: nil] } + let(:args) { { dry_run: nil } } it_behaves_like 'a real run' end context 'when dry_run is true' do - let(:args) { [dry_run: true] } + let(:args) { { dry_run: true } } it_behaves_like 'a dry run' end context 'with dry_run not specified' do - let(:args) { [] } + let(:args) { {} } it_behaves_like 'a dry run' end diff --git a/spec/lib/gitlab/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb index c72efa66024..0153cfbf091 100644 --- a/spec/lib/gitlab/config/entry/configurable_spec.rb +++ b/spec/lib/gitlab/config/entry/configurable_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Gitlab::Config::Entry::Configurable do describe 'validations' do context 'when entry is a hash' do - let(:instance) { entry.new(key: 'value') } + let(:instance) { entry.new({ key: 'value' }) } it 'correctly validates an instance' do expect(instance).to be_valid diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 2c5988f06b2..553f33a66c4 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -40,6 +40,9 @@ RSpec.describe 'value stream analytics events', :aggregate_failures do before do create_commit_referencing_issue(context) + + # Adding extra duration because the new VSA backend filters out 0 durations between these columns + context.metrics.update!(first_mentioned_in_commit_at: context.metrics.first_associated_with_milestone_at + 1.day) end it 'has correct attributes' do diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 719d4a69985..21503dc1501 100644 --- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do project.add_maintainer(user) end - let(:stage_summary) { described_class.new(project, options).data } + let(:stage_summary) { described_class.new(project, **options).data } describe "#new_issues" do subject { stage_summary.first } @@ -121,7 +121,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do end it 'does not include commit stats' do - data = described_class.new(project, options).data + data = described_class.new(project, **options).data expect(includes_commits?(data)).to be_falsy end diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb deleted file mode 100644 index 9ebdacb16de..00000000000 --- a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::CycleAnalytics::UsageData do - describe '#to_json' do - before do - # Since git commits only have second precision, round up to the - # nearest second to ensure we have accurate median and standard - # deviation calculations. - current_time = Time.at(Time.now.to_i) - - Timecop.freeze(current_time) do - user = create(:user, :admin) - projects = create_list(:project, 2, :repository) - - projects.each_with_index do |project, time| - issue = create(:issue, project: project, created_at: (time + 1).hour.ago) - - allow_next_instance_of(Gitlab::ReferenceExtractor) do |instance| - allow(instance).to receive(:issues).and_return([issue]) - end - - milestone = create(:milestone, project: project) - mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") - pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) - - create_cycle(user, project, issue, mr, milestone, pipeline) - deploy_master(user, project, environment: 'staging') - deploy_master(user, project) - end - end - end - - context 'a valid usage data result' do - let(:expect_values_per_stage) do - { - issue: { - average: 5400, - sd: 2545, - missing: 0 - }, - plan: { - average: 1, - sd: 0, - missing: 0 - }, - code: { - average: nil, - sd: 0, - missing: 2 - }, - test: { - average: nil, - sd: 0, - missing: 2 - }, - review: { - average: 0, - sd: 0, - missing: 0 - }, - staging: { - average: 0, - sd: 0, - missing: 0 - }, - production: { - average: 5400, - sd: 2545, - missing: 0 - } - } - end - - it 'returns the aggregated usage data of every selected project', :sidekiq_might_not_need_inline do - result = subject.to_json - - expect(result).to have_key(:avg_cycle_analytics) - - CycleAnalytics::LevelBase::STAGES.each do |stage| - expect(result[:avg_cycle_analytics]).to have_key(stage) - - stage_values = result[:avg_cycle_analytics][stage] - expected_values = expect_values_per_stage[stage] - - expected_values.each_pair do |op, value| - expect(stage_values).to have_key(op) - expect(stage_values[op]).to eq(value) - end - end - end - end - end -end diff --git a/spec/lib/gitlab/danger/base_linter_spec.rb b/spec/lib/gitlab/danger/base_linter_spec.rb new file mode 100644 index 00000000000..bd0ceb5a125 --- /dev/null +++ b/spec/lib/gitlab/danger/base_linter_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_relative 'danger_spec_helper' + +require 'gitlab/danger/base_linter' + +RSpec.describe Gitlab::Danger::BaseLinter do + let(:commit_class) do + Struct.new(:message, :sha, :diff_parent) + end + + let(:commit_message) { 'A commit message' } + let(:commit) { commit_class.new(commit_message, anything, anything) } + + subject(:commit_linter) { described_class.new(commit) } + + describe '#failed?' do + context 'with no failures' do + it { expect(commit_linter).not_to be_failed } + end + + context 'with failures' do + before do + commit_linter.add_problem(:subject_too_long, described_class.subject_description) + end + + it { expect(commit_linter).to be_failed } + end + end + + describe '#add_problem' do + it 'stores messages in #failures' do + commit_linter.add_problem(:subject_too_long, '%s') + + expect(commit_linter.problems).to eq({ subject_too_long: described_class.problems_mapping[:subject_too_long] }) + end + end + + shared_examples 'a valid commit' do + it 'does not have any problem' do + commit_linter.lint_subject + + expect(commit_linter.problems).to be_empty + end + end + + describe '#lint_subject' do + context 'when subject valid' do + it_behaves_like 'a valid commit' + end + + context 'when subject is too short' do + let(:commit_message) { 'A B' } + + it 'adds a problem' do + expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class.subject_description) + + commit_linter.lint_subject + end + end + + context 'when subject is too long' do + let(:commit_message) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH } + + it 'adds a problem' do + expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description) + + commit_linter.lint_subject + end + end + + context 'when subject is a WIP' do + let(:final_message) { 'A B C' } + # commit message with prefix will be over max length. commit message without prefix will be of maximum size + let(:commit_message) { described_class::WIP_PREFIX + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) } + + it 'does not have any problems' do + commit_linter.lint_subject + + expect(commit_linter.problems).to be_empty + end + end + + context 'when subject is too short and too long' do + let(:commit_message) { 'A ' + 'B' * described_class::MAX_LINE_LENGTH } + + it 'adds a problem' do + expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class.subject_description) + expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description) + + commit_linter.lint_subject + end + end + + context 'when subject starts with lowercase' do + let(:commit_message) { 'a B C' } + + it 'adds a problem' do + expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class.subject_description) + + commit_linter.lint_subject + end + end + + [ + '[ci skip] A commit message', + '[Ci skip] A commit message', + '[API] A commit message', + 'api: A commit message', + 'API: A commit message', + 'API: a commit message', + 'API: a commit message' + ].each do |message| + context "when subject is '#{message}'" do + let(:commit_message) { message } + + it 'does not add a problem' do + expect(commit_linter).not_to receive(:add_problem) + + commit_linter.lint_subject + end + end + end + + [ + '[ci skip]A commit message', + '[Ci skip] A commit message', + '[ci skip] a commit message', + 'api: a commit message', + '! A commit message' + ].each do |message| + context "when subject is '#{message}'" do + let(:commit_message) { message } + + it 'adds a problem' do + expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class.subject_description) + + commit_linter.lint_subject + end + end + end + + context 'when subject ends with a period' do + let(:commit_message) { 'A B C.' } + + it 'adds a problem' do + expect(commit_linter).to receive(:add_problem).with(:subject_ends_with_a_period, described_class.subject_description) + + commit_linter.lint_subject + end + end + end +end diff --git a/spec/lib/gitlab/danger/commit_linter_spec.rb b/spec/lib/gitlab/danger/commit_linter_spec.rb index ebfeedba700..d3d86037a53 100644 --- a/spec/lib/gitlab/danger/commit_linter_spec.rb +++ b/spec/lib/gitlab/danger/commit_linter_spec.rb @@ -98,28 +98,6 @@ RSpec.describe Gitlab::Danger::CommitLinter do end end - describe '#failed?' do - context 'with no failures' do - it { expect(commit_linter).not_to be_failed } - end - - context 'with failures' do - before do - commit_linter.add_problem(:details_line_too_long) - end - - it { expect(commit_linter).to be_failed } - end - end - - describe '#add_problem' do - it 'stores messages in #failures' do - commit_linter.add_problem(:details_line_too_long) - - expect(commit_linter.problems).to eq({ details_line_too_long: described_class::PROBLEMS[:details_line_too_long] }) - end - end - shared_examples 'a valid commit' do it 'does not have any problem' do commit_linter.lint @@ -129,113 +107,6 @@ RSpec.describe Gitlab::Danger::CommitLinter do end describe '#lint' do - describe 'subject' do - context 'when subject valid' do - it_behaves_like 'a valid commit' - end - - context 'when subject is too short' do - let(:commit_message) { 'A B' } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class::DEFAULT_SUBJECT_DESCRIPTION) - - commit_linter.lint - end - end - - context 'when subject is too long' do - let(:commit_message) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class::DEFAULT_SUBJECT_DESCRIPTION) - - commit_linter.lint - end - end - - context 'when subject is a WIP' do - let(:final_message) { 'A B C' } - # commit message with prefix will be over max length. commit message without prefix will be of maximum size - let(:commit_message) { described_class::WIP_PREFIX + final_message + 'D' * (described_class::MAX_LINE_LENGTH - final_message.size) } - - it 'does not have any problems' do - commit_linter.lint - - expect(commit_linter.problems).to be_empty - end - end - - context 'when subject is too short and too long' do - let(:commit_message) { 'A ' + 'B' * described_class::MAX_LINE_LENGTH } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_too_short, described_class::DEFAULT_SUBJECT_DESCRIPTION) - expect(commit_linter).to receive(:add_problem).with(:subject_too_long, described_class::DEFAULT_SUBJECT_DESCRIPTION) - - commit_linter.lint - end - end - - context 'when subject starts with lowercase' do - let(:commit_message) { 'a B C' } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class::DEFAULT_SUBJECT_DESCRIPTION) - - commit_linter.lint - end - end - - [ - '[ci skip] A commit message', - '[Ci skip] A commit message', - '[API] A commit message', - 'api: A commit message', - 'API: A commit message' - ].each do |message| - context "when subject is '#{message}'" do - let(:commit_message) { message } - - it 'does not add a problem' do - expect(commit_linter).not_to receive(:add_problem) - - commit_linter.lint - end - end - end - - [ - '[ci skip]A commit message', - '[Ci skip] A commit message', - '[ci skip] a commit message', - 'API: a commit message', - 'API: a commit message', - 'api: a commit message', - '! A commit message' - ].each do |message| - context "when subject is '#{message}'" do - let(:commit_message) { message } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_starts_with_lowercase, described_class::DEFAULT_SUBJECT_DESCRIPTION) - - commit_linter.lint - end - end - end - - context 'when subject ends with a period' do - let(:commit_message) { 'A B C.' } - - it 'adds a problem' do - expect(commit_linter).to receive(:add_problem).with(:subject_ends_with_a_period, described_class::DEFAULT_SUBJECT_DESCRIPTION) - - commit_linter.lint - end - end - end - describe 'separator' do context 'when separator is missing' do let(:commit_message) { "A B C\n" } @@ -300,8 +171,10 @@ RSpec.describe Gitlab::Danger::CommitLinter do end end - context 'when details exceeds the max line length including a URL' do - let(:commit_message) { "A B C\n\nhttps://gitlab.com" + 'D' * described_class::MAX_LINE_LENGTH } + context 'when details exceeds the max line length including URLs' do + let(:commit_message) do + "A B C\n\nsome message with https://example.com and https://gitlab.com" + 'D' * described_class::MAX_LINE_LENGTH + end it_behaves_like 'a valid commit' end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index f400641706d..a8f113a8cd1 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -33,6 +33,16 @@ RSpec.describe Gitlab::Danger::Helper do expect(helper.gitlab_helper).to eq(fake_gitlab) end end + + context 'when danger gitlab plugin is not available' do + it 'returns nil' do + invalid_danger = Class.new do + include Gitlab::Danger::Helper + end.new + + expect(invalid_danger.gitlab_helper).to be_nil + end + end end describe '#release_automation?' do @@ -591,4 +601,30 @@ RSpec.describe Gitlab::Danger::Helper do expect(helper.prepare_labels_for_mr([])).to eq('') end end + + describe '#has_ci_changes?' do + context 'when .gitlab/ci is changed' do + it 'returns true' do + expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab/ci/test.yml]) + + expect(helper.has_ci_changes?).to be_truthy + end + end + + context 'when .gitlab-ci.yml is changed' do + it 'returns true' do + expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb .gitlab-ci.yml]) + + expect(helper.has_ci_changes?).to be_truthy + end + end + + context 'when neither .gitlab/ci/ or .gitlab-ci.yml is changed' do + it 'returns false' do + expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb nested/.gitlab-ci.yml]) + + expect(helper.has_ci_changes?).to be_falsey + end + end + end end diff --git a/spec/lib/gitlab/danger/merge_request_linter_spec.rb b/spec/lib/gitlab/danger/merge_request_linter_spec.rb new file mode 100644 index 00000000000..29facc9fdd6 --- /dev/null +++ b/spec/lib/gitlab/danger/merge_request_linter_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' +require_relative 'danger_spec_helper' + +require 'gitlab/danger/merge_request_linter' + +RSpec.describe Gitlab::Danger::MergeRequestLinter do + using RSpec::Parameterized::TableSyntax + + let(:mr_class) do + Struct.new(:message, :sha, :diff_parent) + end + + let(:mr_title) { 'A B ' + 'C' } + let(:merge_request) { mr_class.new(mr_title, anything, anything) } + + describe '#lint_subject' do + subject(:mr_linter) { described_class.new(merge_request) } + + shared_examples 'a valid mr title' do + it 'does not have any problem' do + mr_linter.lint + + expect(mr_linter.problems).to be_empty + end + end + + context 'when subject valid' do + it_behaves_like 'a valid mr title' + end + + context 'when it is too long' do + let(:mr_title) { 'A B ' + 'C' * described_class::MAX_LINE_LENGTH } + + it 'adds a problem' do + expect(mr_linter).to receive(:add_problem).with(:subject_too_long, described_class.subject_description) + + mr_linter.lint + end + end + + describe 'using magic mr run options' do + where(run_option: described_class.mr_run_options_regex.split('|') + + described_class.mr_run_options_regex.split('|').map! { |x| "[#{x}]" }) + + with_them do + let(:mr_title) { run_option + ' A B ' + 'C' * (described_class::MAX_LINE_LENGTH - 5) } + + it_behaves_like 'a valid mr title' + end + end + end +end diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb index 1a900dfba22..561e108bf31 100644 --- a/spec/lib/gitlab/danger/roulette_spec.rb +++ b/spec/lib/gitlab/danger/roulette_spec.rb @@ -165,6 +165,14 @@ RSpec.describe Gitlab::Danger::Roulette do end end + context 'when change contains many categories' do + let(:categories) { [:frontend, :test, :qa, :engineering_productivity, :ci_template, :backend] } + + it 'has a deterministic sorting order' do + expect(spins.map(&:category)).to eq categories.sort + end + end + context 'when change contains QA category' do let(:categories) { [:qa] } diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index 4e0cc8a1fa9..e5dfff33a2a 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -21,9 +21,10 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do let(:data) { described_class.build(pipeline) } let(:attributes) { data[:object_attributes] } let(:build_data) { data[:builds].first } + let(:runner_data) { build_data[:runner] } let(:project_data) { data[:project] } - it 'has correct attributes' do + it 'has correct attributes', :aggregate_failures do expect(attributes).to be_a(Hash) expect(attributes[:ref]).to eq(pipeline.ref) expect(attributes[:sha]).to eq(pipeline.sha) @@ -36,6 +37,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do expect(build_data[:id]).to eq(build.id) expect(build_data[:status]).to eq(build.status) expect(build_data[:allow_failure]).to eq(build.allow_failure) + expect(runner_data).to eq(nil) expect(project_data).to eq(project.hook_attrs(backward: false)) expect(data[:merge_request]).to be_nil expect(data[:user]).to eq({ @@ -46,6 +48,18 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do }) end + context 'build with runner' do + let!(:build) { create(:ci_build, pipeline: pipeline, runner: ci_runner) } + let(:ci_runner) { create(:ci_runner) } + + it 'has runner attributes', :aggregate_failures do + expect(runner_data[:id]).to eq(ci_runner.id) + expect(runner_data[:description]).to eq(ci_runner.description) + expect(runner_data[:active]).to eq(ci_runner.active) + expect(runner_data[:is_shared]).to eq(ci_runner.instance_type?) + end + end + context 'pipeline without variables' do it 'has empty variables hash' do expect(attributes[:variables]).to be_a(Array) diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb index a1cc759e011..29688b18e94 100644 --- a/spec/lib/gitlab/database/batch_count_spec.rb +++ b/spec/lib/gitlab/database/batch_count_spec.rb @@ -130,6 +130,16 @@ RSpec.describe Gitlab::Database::BatchCount do expect(described_class.batch_count(model, start: model.minimum(:id), finish: model.maximum(:id))).to eq(5) end + it 'stops counting when finish value is reached' do + stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0) + + expect(described_class.batch_count(model, + start: model.minimum(:id), + finish: model.maximum(:id) - 1, # Do not count the last record + batch_size: model.count - 2 # Ensure there are multiple batches + )).to eq(model.count - 1) + end + it "defaults the batch size to #{Gitlab::Database::BatchCounter::DEFAULT_BATCH_SIZE}" do min_id = model.minimum(:id) relation = instance_double(ActiveRecord::Relation) @@ -242,6 +252,19 @@ RSpec.describe Gitlab::Database::BatchCount do expect(described_class.batch_distinct_count(model, column, start: model.minimum(column), finish: model.maximum(column))).to eq(2) end + it 'stops counting when finish value is reached' do + # Create a new unique author that should not be counted + create(:issue) + + stub_const('Gitlab::Database::BatchCounter::MIN_REQUIRED_BATCH_SIZE', 0) + + expect(described_class.batch_distinct_count(model, column, + start: User.minimum(:id), + finish: User.maximum(:id) - 1, # Do not count the newly created issue + batch_size: model.count - 2 # Ensure there are multiple batches + )).to eq(2) + end + it 'counts with User min and max as start and finish' do expect(described_class.batch_distinct_count(model, column, start: User.minimum(:id), finish: User.maximum(:id))).to eq(2) end diff --git a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb index 48132d68031..3e8563376ce 100644 --- a/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/background_migration_helpers_spec.rb @@ -189,7 +189,51 @@ RSpec.describe Gitlab::Database::Migrations::BackgroundMigrationHelpers do end end - context "when the model doesn't have an ID column" do + context 'when the model specifies a primary_column_name' do + let!(:id1) { create(:container_expiration_policy).id } + let!(:id2) { create(:container_expiration_policy).id } + let!(:id3) { create(:container_expiration_policy).id } + + around do |example| + freeze_time { example.run } + end + + before do + ContainerExpirationPolicy.class_eval do + include EachBatch + end + end + + it 'returns the final expected delay', :aggregate_failures do + Sidekiq::Testing.fake! do + final_delay = model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, batch_size: 2, primary_column_name: :project_id) + + expect(final_delay.to_f).to eq(20.minutes.to_f) + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]]) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.minutes.from_now.to_f) + end + end + + context "when the primary_column_name is not an integer" do + it 'raises error' do + expect do + model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, primary_column_name: :enabled) + end.to raise_error(StandardError, /is not an integer column/) + end + end + + context "when the primary_column_name does not exist" do + it 'raises error' do + expect do + model.queue_background_migration_jobs_by_range_at_intervals(ContainerExpirationPolicy, 'FooJob', 10.minutes, primary_column_name: :foo) + end.to raise_error(StandardError, /does not have an ID column of foo/) + end + end + end + + context "when the model doesn't have an ID or primary_column_name column" do it 'raises error (for now)' do expect do model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds) diff --git a/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb b/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb new file mode 100644 index 00000000000..934e2274358 --- /dev/null +++ b/spec/lib/gitlab/database/postgres_hll/batch_distinct_counter_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresHll::BatchDistinctCounter do + let_it_be(:error_rate) { described_class::ERROR_RATE } # HyperLogLog is a probabilistic algorithm, which provides estimated data, with given error margin + let_it_be(:fallback) { ::Gitlab::Database::BatchCounter::FALLBACK } + let_it_be(:small_batch_size) { calculate_batch_size(described_class::MIN_REQUIRED_BATCH_SIZE) } + let(:model) { Issue } + let(:column) { :author_id } + + let(:in_transaction) { false } + + let_it_be(:user) { create(:user, email: 'email1@domain.com') } + let_it_be(:another_user) { create(:user, email: 'email2@domain.com') } + + def calculate_batch_size(batch_size) + zero_offset_modifier = -1 + + batch_size + zero_offset_modifier + end + + before do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(in_transaction) + end + + context 'different distribution of relation records' do + [10, 100, 100_000].each do |spread| + context "records are spread within #{spread}" do + before do + ids = (1..spread).to_a.sample(10) + create_list(:issue, 10).each_with_index do |issue, i| + issue.id = ids[i] + end + end + + it 'counts table' do + expect(described_class.new(model).estimate_distinct_count).to be_within(error_rate).percent_of(10) + end + end + end + end + + context 'unit test for different counting parameters' do + before_all do + create_list(:issue, 3, author: user) + create_list(:issue, 2, author: another_user) + end + + describe '#estimate_distinct_count' do + it 'counts table' do + expect(described_class.new(model).estimate_distinct_count).to be_within(error_rate).percent_of(5) + end + + it 'counts with column field' do + expect(described_class.new(model, column).estimate_distinct_count).to be_within(error_rate).percent_of(2) + end + + it 'counts with :id field' do + expect(described_class.new(model, :id).estimate_distinct_count).to be_within(error_rate).percent_of(5) + end + + it 'counts with "id" field' do + expect(described_class.new(model, "id").estimate_distinct_count).to be_within(error_rate).percent_of(5) + end + + it 'counts with table.column field' do + expect(described_class.new(model, "#{model.table_name}.#{column}").estimate_distinct_count).to be_within(error_rate).percent_of(2) + end + + it 'counts with Arel column' do + expect(described_class.new(model, model.arel_table[column]).estimate_distinct_count).to be_within(error_rate).percent_of(2) + end + + it 'counts over joined relations' do + expect(described_class.new(model.joins(:author), "users.email").estimate_distinct_count).to be_within(error_rate).percent_of(2) + end + + it 'counts with :column field with batch_size of 50K' do + expect(described_class.new(model, column).estimate_distinct_count(batch_size: 50_000)).to be_within(error_rate).percent_of(2) + end + + it 'will not count table with a batch size less than allowed' do + expect(described_class.new(model, column).estimate_distinct_count(batch_size: small_batch_size)).to eq(fallback) + end + + it 'counts with different number of batches and aggregates total result' do + stub_const('Gitlab::Database::PostgresHll::BatchDistinctCounter::MIN_REQUIRED_BATCH_SIZE', 0) + + [1, 2, 4, 5, 6].each { |i| expect(described_class.new(model).estimate_distinct_count(batch_size: i)).to be_within(error_rate).percent_of(5) } + end + + it 'counts with a start and finish' do + expect(described_class.new(model, column).estimate_distinct_count(start: model.minimum(:id), finish: model.maximum(:id))).to be_within(error_rate).percent_of(2) + end + + it "defaults the batch size to #{Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE}" do + min_id = model.minimum(:id) + batch_end_id = min_id + calculate_batch_size(Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE) + + expect(model).to receive(:where).with("id" => min_id..batch_end_id).and_call_original + + described_class.new(model).estimate_distinct_count + end + + context 'when a transaction is open' do + let(:in_transaction) { true } + + it 'raises an error' do + expect { described_class.new(model, column).estimate_distinct_count }.to raise_error('BatchCount can not be run inside a transaction') + end + end + + context 'disallowed configurations' do + let(:default_batch_size) { Gitlab::Database::PostgresHll::BatchDistinctCounter::DEFAULT_BATCH_SIZE } + + it 'returns fallback if start is bigger than finish' do + expect(described_class.new(model, column).estimate_distinct_count(start: 1, finish: 0)).to eq(fallback) + end + + it 'returns fallback if data volume exceeds upper limit' do + large_finish = Gitlab::Database::PostgresHll::BatchDistinctCounter::MAX_DATA_VOLUME + 1 + expect(described_class.new(model, column).estimate_distinct_count(start: 1, finish: large_finish)).to eq(fallback) + end + + it 'returns fallback if batch size is less than min required' do + expect(described_class.new(model, column).estimate_distinct_count(batch_size: small_batch_size)).to eq(fallback) + end + end + end + end +end diff --git a/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb b/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb new file mode 100644 index 00000000000..da4422bd442 --- /dev/null +++ b/spec/lib/gitlab/database/postgres_index_bloat_estimate_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresIndexBloatEstimate do + before do + ActiveRecord::Base.connection.execute(<<~SQL) + ANALYZE schema_migrations + SQL + end + + subject { described_class.find(identifier) } + + let(:identifier) { 'public.schema_migrations_pkey' } + + describe '#bloat_size' do + it 'returns the bloat size in bytes' do + # We cannot reach much more about the bloat size estimate here + expect(subject.bloat_size).to be >= 0 + end + end + + describe '#bloat_size_bytes' do + it 'is an alias of #bloat_size' do + expect(subject.bloat_size_bytes).to eq(subject.bloat_size) + end + end + + describe '#index' do + it 'belongs to a PostgresIndex' do + expect(subject.index.identifier).to eq(identifier) + end + end +end diff --git a/spec/lib/gitlab/database/postgres_index_spec.rb b/spec/lib/gitlab/database/postgres_index_spec.rb index d65b638f7bc..2fda9b85c5a 100644 --- a/spec/lib/gitlab/database/postgres_index_spec.rb +++ b/spec/lib/gitlab/database/postgres_index_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Database::PostgresIndex do expect(described_class.regular).to all(have_attributes(unique: false)) end - it 'only non partitioned indexes ' do + it 'only non partitioned indexes' do expect(described_class.regular).to all(have_attributes(partitioned: false)) end @@ -46,9 +46,24 @@ RSpec.describe Gitlab::Database::PostgresIndex do end end - describe '.random_few' do - it 'limits to two records by default' do - expect(described_class.random_few(2).size).to eq(2) + describe '#bloat_size' do + subject { build(:postgres_index, bloat_estimate: bloat_estimate) } + + let(:bloat_estimate) { build(:postgres_index_bloat_estimate) } + let(:bloat_size) { double } + + it 'returns the bloat size from the estimate' do + expect(bloat_estimate).to receive(:bloat_size).and_return(bloat_size) + + expect(subject.bloat_size).to eq(bloat_size) + end + + context 'without a bloat estimate available' do + let(:bloat_estimate) { nil } + + it 'returns 0' do + expect(subject.bloat_size).to eq(0) + end end end diff --git a/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb b/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb new file mode 100644 index 00000000000..6e1e53e0e41 --- /dev/null +++ b/spec/lib/gitlab/database/postgresql_adapter/empty_query_ping_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::PostgresqlAdapter::EmptyQueryPing do + describe '#active?' do + let(:adapter_class) do + Class.new do + include Gitlab::Database::PostgresqlAdapter::EmptyQueryPing + + def initialize(connection, lock) + @connection = connection + @lock = lock + end + end + end + + subject { adapter_class.new(connection, lock).active? } + + let(:connection) { double(query: nil) } + let(:lock) { double } + + before do + allow(lock).to receive(:synchronize).and_yield + end + + it 'uses an empty query to check liveness' do + expect(connection).to receive(:query).with(';') + + subject + end + + it 'returns true if no error was signaled' do + expect(subject).to be_truthy + end + + it 'returns false when an error occurs' do + expect(lock).to receive(:synchronize).and_raise(PG::Error) + + expect(subject).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb index 2d6765aac2e..51fc7c6620b 100644 --- a/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb +++ b/spec/lib/gitlab/database/reindexing/concurrent_reindex_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do let(:table_name) { '_test_reindex_table' } let(:column_name) { '_test_column' } let(:index_name) { '_test_reindex_index' } - let(:index) { instance_double(Gitlab::Database::PostgresIndex, indexrelid: 42, name: index_name, schema: 'public', partitioned?: false, unique?: false, exclusion?: false, definition: 'CREATE INDEX _test_reindex_index ON public._test_reindex_table USING btree (_test_column)') } + let(:index) { instance_double(Gitlab::Database::PostgresIndex, indexrelid: 42, name: index_name, schema: 'public', tablename: table_name, partitioned?: false, unique?: false, exclusion?: false, expression?: false, definition: 'CREATE INDEX _test_reindex_index ON public._test_reindex_table USING btree (_test_column)') } let(:logger) { double('logger', debug: nil, info: nil, error: nil ) } let(:connection) { ActiveRecord::Base.connection } @@ -130,6 +130,36 @@ RSpec.describe Gitlab::Database::Reindexing::ConcurrentReindex, '#perform' do check_index_exists end + context 'for expression indexes' do + before do + allow(index).to receive(:expression?).and_return(true) + end + + it 'rebuilds table statistics before dropping the original index' do + expect(connection).to receive(:execute).with('SET statement_timeout TO \'21600s\'').twice + + expect_to_execute_concurrently_in_order(create_index) + + expect_to_execute_concurrently_in_order(<<~SQL) + ANALYZE "#{index.schema}"."#{index.tablename}" + SQL + + expect_next_instance_of(::Gitlab::Database::WithLockRetries) do |instance| + expect(instance).to receive(:run).with(raise_on_exhaustion: true).and_yield + end + + expect_index_rename(index.name, replaced_name) + expect_index_rename(replacement_name, index.name) + expect_index_rename(replaced_name, replacement_name) + + expect_to_execute_concurrently_in_order(drop_index) + + subject.perform + + check_index_exists + end + end + context 'when a dangling index is left from a previous run' do before do connection.execute("CREATE INDEX #{replacement_name} ON #{table_name} (#{column_name})") diff --git a/spec/lib/gitlab/database/reindexing/index_selection_spec.rb b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb new file mode 100644 index 00000000000..a5e2f368f40 --- /dev/null +++ b/spec/lib/gitlab/database/reindexing/index_selection_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Reindexing::IndexSelection do + include DatabaseHelpers + + subject { described_class.new(Gitlab::Database::PostgresIndex.all).to_a } + + before do + swapout_view_for_table(:postgres_index_bloat_estimates) + swapout_view_for_table(:postgres_indexes) + end + + def execute(sql) + ActiveRecord::Base.connection.execute(sql) + end + + it 'orders by highest bloat first' do + create_list(:postgres_index, 10).each_with_index do |index, i| + create(:postgres_index_bloat_estimate, index: index, bloat_size_bytes: 1.megabyte * i) + end + + expected = Gitlab::Database::PostgresIndexBloatEstimate.order(bloat_size_bytes: :desc).map(&:index) + + expect(subject).to eq(expected) + end + + context 'with time frozen' do + around do |example| + freeze_time { example.run } + end + + it 'does not return indexes with reindex action in the last 7 days' do + not_recently_reindexed = create_list(:postgres_index, 2).each_with_index do |index, i| + create(:postgres_index_bloat_estimate, index: index, bloat_size_bytes: 1.megabyte * i) + create(:reindex_action, index: index, action_end: Time.zone.now - 7.days - 1.minute) + end + + create_list(:postgres_index, 2).each_with_index do |index, i| + create(:postgres_index_bloat_estimate, index: index, bloat_size_bytes: 1.megabyte * i) + create(:reindex_action, index: index, action_end: Time.zone.now) + end + + expected = Gitlab::Database::PostgresIndexBloatEstimate.where(identifier: not_recently_reindexed.map(&:identifier)).map(&:index).map(&:identifier).sort + + expect(subject.map(&:identifier).sort).to eq(expected) + end + end +end diff --git a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb index efb5b8463a1..225f23d2135 100644 --- a/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb +++ b/spec/lib/gitlab/database/reindexing/reindex_action_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::Reindexing::ReindexAction, '.keep_track_of' do - let(:index) { double('index', identifier: 'public.something', ondisk_size_bytes: 10240, reload: nil) } + let(:index) { double('index', identifier: 'public.something', ondisk_size_bytes: 10240, reload: nil, bloat_size: 42) } let(:size_after) { 512 } it 'yields to the caller' do @@ -47,6 +47,12 @@ RSpec.describe Gitlab::Database::Reindexing::ReindexAction, '.keep_track_of' do expect(find_record.ondisk_size_bytes_end).to eq(size_after) end + it 'creates the record with the indexes bloat estimate' do + described_class.keep_track_of(index) do + expect(find_record.bloat_estimate_bytes_start).to eq(index.bloat_size) + end + end + context 'in case of errors' do it 'sets the state to failed' do expect do diff --git a/spec/lib/gitlab/database/reindexing_spec.rb b/spec/lib/gitlab/database/reindexing_spec.rb index 359e0597f4e..eb78a5fe8ea 100644 --- a/spec/lib/gitlab/database/reindexing_spec.rb +++ b/spec/lib/gitlab/database/reindexing_spec.rb @@ -6,12 +6,16 @@ RSpec.describe Gitlab::Database::Reindexing do include ExclusiveLeaseHelpers describe '.perform' do - subject { described_class.perform(indexes) } + subject { described_class.perform(candidate_indexes) } let(:coordinator) { instance_double(Gitlab::Database::Reindexing::Coordinator) } + let(:index_selection) { instance_double(Gitlab::Database::Reindexing::IndexSelection) } + let(:candidate_indexes) { double } let(:indexes) { double } it 'delegates to Coordinator' do + expect(Gitlab::Database::Reindexing::IndexSelection).to receive(:new).with(candidate_indexes).and_return(index_selection) + expect(index_selection).to receive(:take).with(2).and_return(indexes) expect(Gitlab::Database::Reindexing::Coordinator).to receive(:new).with(indexes).and_return(coordinator) expect(coordinator).to receive(:perform) diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb index ca9f9ab915f..4048fc69591 100644 --- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb +++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb @@ -118,8 +118,8 @@ RSpec.describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService expect(result[:status]).to eq(:success) expect(project.name).to eq(described_class::PROJECT_NAME) expect(project.description).to eq( - 'This project is automatically generated and will be used to help monitor this GitLab instance. ' \ - "[More information](#{docs_path})" + 'This project is automatically generated and helps monitor this GitLab instance. ' \ + "[Learn more](#{docs_path})." ) expect(File).to exist("doc/#{path}.md") end diff --git a/spec/lib/gitlab/deploy_key_access_spec.rb b/spec/lib/gitlab/deploy_key_access_spec.rb new file mode 100644 index 00000000000..e186e993d8f --- /dev/null +++ b/spec/lib/gitlab/deploy_key_access_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::DeployKeyAccess do + let_it_be(:user) { create(:user) } + let_it_be(:deploy_key) { create(:deploy_key, user: user) } + let(:project) { create(:project, :repository) } + let(:protected_branch) { create(:protected_branch, :no_one_can_push, project: project) } + + subject(:access) { described_class.new(deploy_key, container: project) } + + before do + project.add_guest(user) + create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key) + end + + describe '#can_create_tag?' do + context 'push tag that matches a protected tag pattern via a deploy key' do + it 'still pushes that tag' do + create(:protected_tag, project: project, name: 'v*') + + expect(access.can_create_tag?('v0.1.2')).to be_truthy + end + end + end + + describe '#can_push_for_ref?' do + context 'push to a protected branch of this project via a deploy key' do + before do + create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key) + end + + context 'when the project has active deploy key owned by this user' do + it 'returns true' do + expect(access.can_push_for_ref?(protected_branch.name)).to be_truthy + end + end + + context 'when the project has active deploy keys, but not by this user' do + let(:deploy_key) { create(:deploy_key, user: create(:user)) } + + it 'returns false' do + expect(access.can_push_for_ref?(protected_branch.name)).to be_falsey + end + end + + context 'when there is another branch no one can push to' do + let(:another_branch) { create(:protected_branch, :no_one_can_push, name: 'another_branch', project: project) } + + it 'returns false when trying to push to that other branch' do + expect(access.can_push_for_ref?(another_branch.name)).to be_falsey + end + + context 'and the deploy key added for the first protected branch is also added for this other branch' do + it 'returns true for both protected branches' do + create(:protected_branch_push_access_level, protected_branch: another_branch, deploy_key: deploy_key) + + expect(access.can_push_for_ref?(protected_branch.name)).to be_truthy + expect(access.can_push_for_ref?(another_branch.name)).to be_truthy + end + end + end + end + end +end diff --git a/spec/lib/gitlab/diff/file_collection/commit_spec.rb b/spec/lib/gitlab/diff/file_collection/commit_spec.rb index 7773604a638..3d995b36b6f 100644 --- a/spec/lib/gitlab/diff/file_collection/commit_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/commit_spec.rb @@ -4,17 +4,75 @@ require 'spec_helper' RSpec.describe Gitlab::Diff::FileCollection::Commit do let(:project) { create(:project, :repository) } + let(:diffable) { project.commit } - it_behaves_like 'diff statistics' do - let(:collection_default_args) do - { diff_options: {} } - end + let(:collection_default_args) do + { diff_options: {} } + end - let(:diffable) { project.commit } + it_behaves_like 'diff statistics' do let(:stub_path) { 'bar/branch-test.txt' } end - it_behaves_like 'unfoldable diff' do - let(:diffable) { project.commit } + it_behaves_like 'unfoldable diff' + + it_behaves_like 'sortable diff files' do + let(:diffable) { project.commit('913c66a') } + + let(:unsorted_diff_files_paths) do + [ + '.DS_Store', + 'CHANGELOG', + 'MAINTENANCE.md', + 'PROCESS.md', + 'VERSION', + 'encoding/feature-1.txt', + 'encoding/feature-2.txt', + 'encoding/hotfix-1.txt', + 'encoding/hotfix-2.txt', + 'encoding/russian.rb', + 'encoding/test.txt', + 'encoding/テスト.txt', + 'encoding/テスト.xls', + 'files/.DS_Store', + 'files/html/500.html', + 'files/images/logo-black.png', + 'files/images/logo-white.png', + 'files/js/application.js', + 'files/js/commit.js.coffee', + 'files/markdown/ruby-style-guide.md', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb' + ] + end + + let(:sorted_diff_files_paths) do + [ + 'encoding/feature-1.txt', + 'encoding/feature-2.txt', + 'encoding/hotfix-1.txt', + 'encoding/hotfix-2.txt', + 'encoding/russian.rb', + 'encoding/test.txt', + 'encoding/テスト.txt', + 'encoding/テスト.xls', + 'files/html/500.html', + 'files/images/logo-black.png', + 'files/images/logo-white.png', + 'files/js/application.js', + 'files/js/commit.js.coffee', + 'files/markdown/ruby-style-guide.md', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb', + 'files/.DS_Store', + '.DS_Store', + 'CHANGELOG', + 'MAINTENANCE.md', + 'PROCESS.md', + 'VERSION' + ] + end end end diff --git a/spec/lib/gitlab/diff/file_collection/compare_spec.rb b/spec/lib/gitlab/diff/file_collection/compare_spec.rb index dda4513a3a1..f3326f4f03d 100644 --- a/spec/lib/gitlab/diff/file_collection/compare_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/compare_spec.rb @@ -27,4 +27,43 @@ RSpec.describe Gitlab::Diff::FileCollection::Compare do let(:diffable) { Compare.new(raw_compare, project) } let(:stub_path) { '.gitignore' } end + + it_behaves_like 'sortable diff files' do + let(:diffable) { Compare.new(raw_compare, project) } + let(:collection_default_args) do + { + project: diffable.project, + diff_options: {}, + diff_refs: diffable.diff_refs + } + end + + let(:unsorted_diff_files_paths) do + [ + '.DS_Store', + '.gitignore', + '.gitmodules', + 'Gemfile.zip', + 'files/.DS_Store', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb', + 'gitlab-shell' + ] + end + + let(:sorted_diff_files_paths) do + [ + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/ruby/version_info.rb', + 'files/.DS_Store', + '.DS_Store', + '.gitignore', + '.gitmodules', + 'Gemfile.zip', + 'gitlab-shell' + ] + end + end end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb index 72a66b0451e..670c734ce08 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb @@ -18,6 +18,10 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do let(:diff_files) { subject.diff_files } + before do + stub_feature_flags(diffs_gradual_load: false) + end + describe 'initialize' do it 'memoizes pagination_data' do expect(subject.pagination_data).to eq(current_page: 1, next_page: 2, total_pages: 2) @@ -97,6 +101,18 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do expect(collection.diff_files.map(&:new_path)).to eq(expected_batch_files) end end + + context 'with diffs gradual load feature flag enabled' do + let(:batch_page) { 0 } + + before do + stub_feature_flags(diffs_gradual_load: true) + end + + it 'returns correct diff files' do + expect(subject.diffs.map(&:new_path)).to eq(diff_files_relation.page(1).per(batch_size).map(&:new_path)) + end + end end it_behaves_like 'unfoldable diff' do @@ -114,6 +130,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do end let(:diffable) { merge_request.merge_request_diff } + let(:batch_page) { 2 } let(:stub_path) { '.gitignore' } subject do @@ -127,4 +144,18 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do it_behaves_like 'cacheable diff collection' do let(:cacheable_files_count) { batch_size } end + + it_behaves_like 'unsortable diff files' do + let(:diffable) { merge_request.merge_request_diff } + let(:collection_default_args) do + { diff_options: {} } + end + + subject do + described_class.new(merge_request.merge_request_diff, + batch_page, + batch_size, + **collection_default_args) + end + end end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index 429e552278d..03a9b9bd21e 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -54,4 +54,11 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiff do it 'returns a valid instance of a DiffCollection' do expect(diff_files).to be_a(Gitlab::Git::DiffCollection) end + + it_behaves_like 'unsortable diff files' do + let(:diffable) { merge_request.merge_request_diff } + let(:collection_default_args) do + { diff_options: {} } + end + end end diff --git a/spec/lib/gitlab/diff/file_collection_sorter_spec.rb b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb new file mode 100644 index 00000000000..8822fc55c6e --- /dev/null +++ b/spec/lib/gitlab/diff/file_collection_sorter_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Diff::FileCollectionSorter do + let(:diffs) do + [ + double(new_path: '.dir/test', old_path: '.dir/test'), + double(new_path: '', old_path: '.file'), + double(new_path: '1-folder/A-file.ext', old_path: '1-folder/A-file.ext'), + double(new_path: nil, old_path: '1-folder/M-file.ext'), + double(new_path: '1-folder/Z-file.ext', old_path: '1-folder/Z-file.ext'), + double(new_path: '', old_path: '1-folder/nested/A-file.ext'), + double(new_path: '1-folder/nested/M-file.ext', old_path: '1-folder/nested/M-file.ext'), + double(new_path: nil, old_path: '1-folder/nested/Z-file.ext'), + double(new_path: '2-folder/A-file.ext', old_path: '2-folder/A-file.ext'), + double(new_path: '', old_path: '2-folder/M-file.ext'), + double(new_path: '2-folder/Z-file.ext', old_path: '2-folder/Z-file.ext'), + double(new_path: nil, old_path: '2-folder/nested/A-file.ext'), + double(new_path: 'A-file.ext', old_path: 'A-file.ext'), + double(new_path: '', old_path: 'M-file.ext'), + double(new_path: 'Z-file.ext', old_path: 'Z-file.ext') + ] + end + + subject { described_class.new(diffs) } + + describe '#sort' do + let(:sorted_files_paths) { subject.sort.map { |file| file.new_path.presence || file.old_path } } + + it 'returns list sorted directory first' do + expect(sorted_files_paths).to eq([ + '.dir/test', + '1-folder/nested/A-file.ext', + '1-folder/nested/M-file.ext', + '1-folder/nested/Z-file.ext', + '1-folder/A-file.ext', + '1-folder/M-file.ext', + '1-folder/Z-file.ext', + '2-folder/nested/A-file.ext', + '2-folder/A-file.ext', + '2-folder/M-file.ext', + '2-folder/Z-file.ext', + '.file', + 'A-file.ext', + 'M-file.ext', + 'Z-file.ext' + ]) + end + end +end diff --git a/spec/lib/gitlab/diff/lines_unfolder_spec.rb b/spec/lib/gitlab/diff/lines_unfolder_spec.rb index b891f9e8285..4163c0eced5 100644 --- a/spec/lib/gitlab/diff/lines_unfolder_spec.rb +++ b/spec/lib/gitlab/diff/lines_unfolder_spec.rb @@ -188,7 +188,7 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do let(:old_blob) { Blob.decorate(Gitlab::Git::Blob.new(data: raw_old_blob, size: 10)) } let(:diff) do - Gitlab::Git::Diff.new(diff: raw_diff, + Gitlab::Git::Diff.new({ diff: raw_diff, new_path: "build-aux/flatpak/org.gnome.Nautilus.json", old_path: "build-aux/flatpak/org.gnome.Nautilus.json", a_mode: "100644", @@ -196,7 +196,7 @@ RSpec.describe Gitlab::Diff::LinesUnfolder do new_file: false, renamed_file: false, deleted_file: false, - too_large: false) + too_large: false }) end let(:diff_file) do diff --git a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb index 2ebfb054a96..32b451f8329 100644 --- a/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/service_desk_handler_spec.rb @@ -254,7 +254,7 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do new_issue = Issue.last - expect(new_issue.service_desk_reply_to).to eq('finn@adventuretime.ooo') + expect(new_issue.external_author).to eq('finn@adventuretime.ooo') end end diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index 575ff7f357b..bc4c6cf007d 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -6,7 +6,7 @@ require "spec_helper" RSpec.describe Gitlab::Email::ReplyParser do describe '#execute' do def test_parse_body(mail_string, params = {}) - described_class.new(Mail::Message.new(mail_string), params).execute + described_class.new(Mail::Message.new(mail_string), **params).execute end it "returns an empty string if the message is blank" do diff --git a/spec/lib/gitlab/email/smime/certificate_spec.rb b/spec/lib/gitlab/email/smime/certificate_spec.rb index e4a085d971b..f7bb933e348 100644 --- a/spec/lib/gitlab/email/smime/certificate_spec.rb +++ b/spec/lib/gitlab/email/smime/certificate_spec.rb @@ -69,8 +69,8 @@ RSpec.describe Gitlab::Email::Smime::Certificate do describe '.from_files' do it 'parses correctly a certificate and key' do - allow(File).to receive(:read).with('a_key').and_return(@cert[:key].to_s) - allow(File).to receive(:read).with('a_cert').and_return(@cert[:cert].to_pem) + stub_file_read('a_key', content: @cert[:key].to_s) + stub_file_read('a_cert', content: @cert[:cert].to_pem) parsed_cert = described_class.from_files('a_key', 'a_cert') @@ -79,9 +79,9 @@ RSpec.describe Gitlab::Email::Smime::Certificate do context 'with optional ca_certs' do it 'parses correctly certificate, key and ca_certs' do - allow(File).to receive(:read).with('a_key').and_return(@cert[:key].to_s) - allow(File).to receive(:read).with('a_cert').and_return(@cert[:cert].to_pem) - allow(File).to receive(:read).with('a_ca_cert').and_return(@intermediate_ca[:cert].to_pem) + stub_file_read('a_key', content: @cert[:key].to_s) + stub_file_read('a_cert', content: @cert[:cert].to_pem) + stub_file_read('a_ca_cert', content: @intermediate_ca[:cert].to_pem) parsed_cert = described_class.from_files('a_key', 'a_cert', 'a_ca_cert') @@ -94,8 +94,8 @@ RSpec.describe Gitlab::Email::Smime::Certificate do it 'parses correctly a certificate and key' do cert = generate_cert(signer_ca: @root_ca) - allow(File).to receive(:read).with('a_key').and_return(cert[:key].to_s) - allow(File).to receive(:read).with('a_cert').and_return(cert[:cert].to_pem) + stub_file_read('a_key', content: cert[:key].to_s) + stub_file_read('a_cert', content: cert[:cert].to_pem) parsed_cert = described_class.from_files('a_key', 'a_cert') diff --git a/spec/lib/gitlab/encrypted_configuration_spec.rb b/spec/lib/gitlab/encrypted_configuration_spec.rb new file mode 100644 index 00000000000..eadc2cf71a7 --- /dev/null +++ b/spec/lib/gitlab/encrypted_configuration_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::EncryptedConfiguration do + subject(:configuration) { described_class.new } + + let!(:config_tmp_dir) { Dir.mktmpdir('config-') } + + after do + FileUtils.rm_f(config_tmp_dir) + end + + describe '#initialize' do + it 'accepts all args as optional fields' do + expect { configuration }.not_to raise_exception + + expect(configuration.key).to be_nil + expect(configuration.previous_keys).to be_empty + end + + it 'generates 32 byte key when provided a larger base key' do + configuration = described_class.new(base_key: 'A' * 64) + + expect(configuration.key.bytesize).to eq 32 + end + + it 'generates 32 byte key when provided a smaller base key' do + configuration = described_class.new(base_key: 'A' * 16) + + expect(configuration.key.bytesize).to eq 32 + end + + it 'throws an error when the base key is too small' do + expect { described_class.new(base_key: 'A' * 12) }.to raise_error 'Base key too small' + end + end + + context 'when provided a config file but no key' do + let(:config_path) { File.join(config_tmp_dir, 'credentials.yml.enc') } + + it 'throws an error when writing without a key' do + expect { described_class.new(content_path: config_path).write('test') }.to raise_error Gitlab::EncryptedConfiguration::MissingKeyError + end + + it 'throws an error when reading without a key' do + config = described_class.new(content_path: config_path) + File.write(config_path, 'test') + expect { config.read }.to raise_error Gitlab::EncryptedConfiguration::MissingKeyError + end + end + + context 'when provided key and config file' do + let(:credentials_config_path) { File.join(config_tmp_dir, 'credentials.yml.enc') } + let(:credentials_key) { SecureRandom.hex(64) } + + describe '#write' do + it 'encrypts the file using the provided key' do + encryptor = ActiveSupport::MessageEncryptor.new(Gitlab::EncryptedConfiguration.generate_key(credentials_key), cipher: 'aes-256-gcm') + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write('sample-content') + expect(encryptor.decrypt_and_verify(File.read(credentials_config_path))).to eq('sample-content') + end + end + + describe '#read' do + it 'reads yaml configuration' do + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write({ foo: { bar: true } }.to_yaml) + expect(config[:foo][:bar]).to be true + end + + it 'allows referencing top level keys via dot syntax' do + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write({ foo: { bar: true } }.to_yaml) + expect(config.foo[:bar]).to be true + end + + it 'throws a custom error when referencing an invalid key map config' do + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write("stringcontent") + expect { config[:foo] }.to raise_error Gitlab::EncryptedConfiguration::InvalidConfigError + end + end + + describe '#change' do + it 'changes yaml configuration' do + config = described_class.new(content_path: credentials_config_path, base_key: credentials_key) + + config.write({ foo: { bar: true } }.to_yaml) + config.change do |unencrypted_contents| + contents = YAML.safe_load(unencrypted_contents, permitted_classes: [Symbol]) + contents.merge(beef: "stew").to_yaml + end + expect(config.foo[:bar]).to be true + expect(config.beef).to eq('stew') + end + end + + context 'when provided previous_keys for rotation' do + let(:credential_key_original) { SecureRandom.hex(64) } + let(:credential_key_latest) { SecureRandom.hex(64) } + let(:config_path_original) { File.join(config_tmp_dir, 'credentials-orig.yml.enc') } + let(:config_path_latest) { File.join(config_tmp_dir, 'credentials-latest.yml.enc') } + + def encryptor(key) + ActiveSupport::MessageEncryptor.new(Gitlab::EncryptedConfiguration.generate_key(key), cipher: 'aes-256-gcm') + end + + describe '#write' do + it 'rotates the key when provided a new key' do + config1 = described_class.new(content_path: config_path_original, base_key: credential_key_original) + config1.write('sample-content1') + + config2 = described_class.new(content_path: config_path_latest, base_key: credential_key_latest, previous_keys: [credential_key_original]) + config2.write('sample-content2') + + original_key_encryptor = encryptor(credential_key_original) # can read with the initial key + latest_key_encryptor = encryptor(credential_key_latest) # can read with the new key + both_key_encryptor = encryptor(credential_key_latest) # can read with either key + both_key_encryptor.rotate(Gitlab::EncryptedConfiguration.generate_key(credential_key_original)) + + expect(original_key_encryptor.decrypt_and_verify(File.read(config_path_original))).to eq('sample-content1') + expect(both_key_encryptor.decrypt_and_verify(File.read(config_path_original))).to eq('sample-content1') + expect(latest_key_encryptor.decrypt_and_verify(File.read(config_path_latest))).to eq('sample-content2') + expect(both_key_encryptor.decrypt_and_verify(File.read(config_path_latest))).to eq('sample-content2') + expect { original_key_encryptor.decrypt_and_verify(File.read(config_path_latest)) }.to raise_error(ActiveSupport::MessageEncryptor::InvalidMessage) + end + end + + describe '#read' do + it 'supports reading using rotated config' do + described_class.new(content_path: config_path_original, base_key: credential_key_original).write({ foo: { bar: true } }.to_yaml) + + config = described_class.new(content_path: config_path_original, base_key: credential_key_latest, previous_keys: [credential_key_original]) + expect(config[:foo][:bar]).to be true + end + end + end + end +end diff --git a/spec/lib/gitlab/experimentation/controller_concern_spec.rb b/spec/lib/gitlab/experimentation/controller_concern_spec.rb index 2fe3d36daf7..03cb89ee033 100644 --- a/spec/lib/gitlab/experimentation/controller_concern_spec.rb +++ b/spec/lib/gitlab/experimentation/controller_concern_spec.rb @@ -6,12 +6,10 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do before do stub_const('Gitlab::Experimentation::EXPERIMENTS', { backwards_compatible_test_experiment: { - environment: environment, tracking_category: 'Team', use_backwards_compatible_subject_index: true }, test_experiment: { - environment: environment, tracking_category: 'Team' } } @@ -21,7 +19,6 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do Feature.enable_percentage_of_time(:test_experiment_experiment_percentage, enabled_percentage) end - let(:environment) { Rails.env.test? } let(:enabled_percentage) { 10 } controller(ApplicationController) do @@ -78,29 +75,24 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do describe '#push_frontend_experiment' do it 'pushes an experiment to the frontend' do gon = instance_double('gon') - experiments = { experiments: { 'myExperiment' => true } } - - stub_experiment_for_user(my_experiment: true) + stub_experiment_for_subject(my_experiment: true) allow(controller).to receive(:gon).and_return(gon) - expect(gon).to receive(:push).with(experiments, true) + expect(gon).to receive(:push).with({ experiments: { 'myExperiment' => true } }, true) controller.push_frontend_experiment(:my_experiment) end end describe '#experiment_enabled?' do - def check_experiment(exp_key = :test_experiment) - controller.experiment_enabled?(exp_key) + def check_experiment(exp_key = :test_experiment, subject = nil) + controller.experiment_enabled?(exp_key, subject: subject) end subject { check_experiment } context 'cookie is not present' do - it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and an experimentation_subject_index of nil' do - expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(:test_experiment, nil) - check_experiment - end + it { is_expected.to eq(false) } end context 'cookie is present' do @@ -112,37 +104,56 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do end where(:experiment_key, :index_value) do - :test_experiment | 40 # Zlib.crc32('test_experimentabcd-1234') % 100 = 40 - :backwards_compatible_test_experiment | 76 # 'abcd1234'.hex % 100 = 76 + :test_experiment | 'abcd-1234' + :backwards_compatible_test_experiment | 'abcd1234' end with_them do - it 'calls Gitlab::Experimentation.enabled_for_value? with the name of the experiment and the calculated experimentation_subject_index based on the uuid' do - expect(Gitlab::Experimentation).to receive(:enabled_for_value?).with(experiment_key, index_value) + it 'calls Gitlab::Experimentation.in_experiment_group?? with the name of the experiment and the calculated experimentation_subject_index based on the uuid' do + expect(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, subject: index_value) + check_experiment(experiment_key) end end - end - it 'returns true when DNT: 0 is set in the request' do - allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true } - controller.request.headers['DNT'] = '0' + context 'when subject is given' do + let(:user) { build(:user) } + + it 'uses the subject' do + expect(Gitlab::Experimentation).to receive(:in_experiment_group?).with(:test_experiment, subject: user) - is_expected.to be_truthy + check_experiment(:test_experiment, user) + end + end end - it 'returns false when DNT: 1 is set in the request' do - allow(Gitlab::Experimentation).to receive(:enabled_for_value?) { true } - controller.request.headers['DNT'] = '1' + context 'do not track' do + before do + allow(Gitlab::Experimentation).to receive(:in_experiment_group?) { true } + end + + context 'when do not track is disabled' do + before do + controller.request.headers['DNT'] = '0' + end + + it { is_expected.to eq(true) } + end - is_expected.to be_falsy + context 'when do not track is enabled' do + before do + controller.request.headers['DNT'] = '1' + end + + it { is_expected.to eq(false) } + end end - describe 'URL parameter to force enable experiment' do + context 'URL parameter to force enable experiment' do it 'returns true unconditionally' do get :index, params: { force_experiment: :test_experiment } - is_expected.to be_truthy + is_expected.to eq(true) end end end @@ -155,7 +166,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do context 'the user is part of the experimental group' do before do - stub_experiment_for_user(test_experiment: true) + stub_experiment_for_subject(test_experiment: true) end it 'tracks the event with the right parameters' do @@ -172,7 +183,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do context 'the user is part of the control group' do before do - stub_experiment_for_user(test_experiment: false) + stub_experiment_for_subject(test_experiment: false) end it 'tracks the event with the right parameters' do @@ -215,6 +226,59 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do expect_no_snowplow_event end end + + context 'subject is provided' do + before do + stub_experiment_for_subject(test_experiment: false) + end + + it "provides the subject's hashed global_id as label" do + experiment_subject = double(:subject, to_global_id: 'abc') + + controller.track_experiment_event(:test_experiment, 'start', 1, subject: experiment_subject) + + expect_snowplow_event( + category: 'Team', + action: 'start', + property: 'control_group', + value: 1, + label: Digest::MD5.hexdigest('abc') + ) + end + + it "provides the subject's hashed string representation as label" do + experiment_subject = 'somestring' + + controller.track_experiment_event(:test_experiment, 'start', 1, subject: experiment_subject) + + expect_snowplow_event( + category: 'Team', + action: 'start', + property: 'control_group', + value: 1, + label: Digest::MD5.hexdigest('somestring') + ) + end + end + + context 'no subject is provided but cookie is set' do + before do + get :index + stub_experiment_for_subject(test_experiment: false) + end + + it 'uses the experimentation_subject_id as fallback' do + controller.track_experiment_event(:test_experiment, 'start', 1) + + expect_snowplow_event( + category: 'Team', + action: 'start', + property: 'control_group', + value: 1, + label: cookies.permanent.signed[:experimentation_subject_id] + ) + end + end end context 'when the experiment is disabled' do @@ -238,7 +302,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do context 'the user is part of the experimental group' do before do - stub_experiment_for_user(test_experiment: true) + stub_experiment_for_subject(test_experiment: true) end it 'pushes the right parameters to gon' do @@ -256,9 +320,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do context 'the user is part of the control group' do before do - allow_next_instance_of(described_class) do |instance| - allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false) - end + stub_experiment_for_subject(test_experiment: false) end it 'pushes the right parameters to gon' do @@ -311,7 +373,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do it 'does not push data to gon' do controller.frontend_experimentation_tracking_data(:test_experiment, 'start') - expect(Gon.method_defined?(:tracking_data)).to be_falsey + expect(Gon.method_defined?(:tracking_data)).to eq(false) end end end @@ -322,7 +384,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do end it 'does not push data to gon' do - expect(Gon.method_defined?(:tracking_data)).to be_falsey + expect(Gon.method_defined?(:tracking_data)).to eq(false) controller.track_experiment_event(:test_experiment, 'start') end end @@ -330,6 +392,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do describe '#record_experiment_user' do let(:user) { build(:user) } + let(:context) { { a: 42 } } context 'when the experiment is enabled' do before do @@ -339,27 +402,25 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do context 'the user is part of the experimental group' do before do - stub_experiment_for_user(test_experiment: true) + stub_experiment_for_subject(test_experiment: true) end it 'calls add_user on the Experiment model' do - expect(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user) + expect(::Experiment).to receive(:add_user).with(:test_experiment, :experimental, user, context) - controller.record_experiment_user(:test_experiment) + controller.record_experiment_user(:test_experiment, context) end end context 'the user is part of the control group' do before do - allow_next_instance_of(described_class) do |instance| - allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false) - end + stub_experiment_for_subject(test_experiment: false) end it 'calls add_user on the Experiment model' do - expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user) + expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user, context) - controller.record_experiment_user(:test_experiment) + controller.record_experiment_user(:test_experiment, context) end end end @@ -373,7 +434,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do it 'does not call add_user on the Experiment model' do expect(::Experiment).not_to receive(:add_user) - controller.record_experiment_user(:test_experiment) + controller.record_experiment_user(:test_experiment, context) end end @@ -385,27 +446,26 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do it 'does not call add_user on the Experiment model' do expect(::Experiment).not_to receive(:add_user) - controller.record_experiment_user(:test_experiment) + controller.record_experiment_user(:test_experiment, context) end end context 'do not track' do before do + stub_experiment(test_experiment: true) allow(controller).to receive(:current_user).and_return(user) - allow_next_instance_of(described_class) do |instance| - allow(instance).to receive(:experiment_enabled?).with(:test_experiment).and_return(false) - end end context 'is disabled' do before do request.headers['DNT'] = '0' + stub_experiment_for_subject(test_experiment: false) end it 'calls add_user on the Experiment model' do - expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user) + expect(::Experiment).to receive(:add_user).with(:test_experiment, :control, user, context) - controller.record_experiment_user(:test_experiment) + controller.record_experiment_user(:test_experiment, context) end end @@ -417,12 +477,62 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do it 'does not call add_user on the Experiment model' do expect(::Experiment).not_to receive(:add_user) - controller.record_experiment_user(:test_experiment) + controller.record_experiment_user(:test_experiment, context) end end end end + describe '#record_experiment_conversion_event' do + let(:user) { build(:user) } + + before do + allow(controller).to receive(:dnt_enabled?).and_return(false) + allow(controller).to receive(:current_user).and_return(user) + stub_experiment(test_experiment: true) + end + + subject(:record_conversion_event) do + controller.record_experiment_conversion_event(:test_experiment) + end + + it 'records the conversion event for the experiment & user' do + expect(::Experiment).to receive(:record_conversion_event).with(:test_experiment, user) + record_conversion_event + end + + shared_examples 'does not record the conversion event' do + it 'does not record the conversion event' do + expect(::Experiment).not_to receive(:record_conversion_event) + record_conversion_event + end + end + + context 'when DNT is enabled' do + before do + allow(controller).to receive(:dnt_enabled?).and_return(true) + end + + include_examples 'does not record the conversion event' + end + + context 'when there is no current user' do + before do + allow(controller).to receive(:current_user).and_return(nil) + end + + include_examples 'does not record the conversion event' + end + + context 'when the experiment is not enabled' do + before do + stub_experiment(test_experiment: false) + end + + include_examples 'does not record the conversion event' + end + end + describe '#experiment_tracking_category_and_group' do let_it_be(:experiment_key) { :test_something } @@ -430,7 +540,7 @@ RSpec.describe Gitlab::Experimentation::ControllerConcern, type: :controller do it 'returns a string with the experiment tracking category & group joined with a ":"' do expect(controller).to receive(:tracking_category).with(experiment_key).and_return('Experiment::Category') - expect(controller).to receive(:tracking_group).with(experiment_key, '_group').and_return('experimental_group') + expect(controller).to receive(:tracking_group).with(experiment_key, '_group', subject: nil).and_return('experimental_group') expect(subject).to eq('Experiment::Category:experimental_group') end diff --git a/spec/lib/gitlab/experimentation/experiment_spec.rb b/spec/lib/gitlab/experimentation/experiment_spec.rb new file mode 100644 index 00000000000..7b1d1763010 --- /dev/null +++ b/spec/lib/gitlab/experimentation/experiment_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Experimentation::Experiment do + using RSpec::Parameterized::TableSyntax + + let(:percentage) { 50 } + let(:params) do + { + tracking_category: 'Category1', + use_backwards_compatible_subject_index: true + } + end + + before do + feature = double('FeatureFlag', percentage_of_time_value: percentage ) + expect(Feature).to receive(:get).with(:experiment_key_experiment_percentage).and_return(feature) + end + + subject(:experiment) { described_class.new(:experiment_key, **params) } + + describe '#active?' do + before do + allow(Gitlab).to receive(:dev_env_or_com?).and_return(on_gitlab_com) + end + + subject { experiment.active? } + + where(:on_gitlab_com, :percentage, :is_active) do + true | 0 | false + true | 10 | true + false | 0 | false + false | 10 | false + end + + with_them do + it { is_expected.to eq(is_active) } + end + end + + describe '#enabled_for_index?' do + subject { experiment.enabled_for_index?(index) } + + where(:index, :percentage, :is_enabled) do + 50 | 40 | false + 40 | 50 | true + nil | 50 | false + end + + with_them do + it { is_expected.to eq(is_enabled) } + end + end +end diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb index ebf98a0151f..a68c050d829 100644 --- a/spec/lib/gitlab/experimentation_spec.rb +++ b/spec/lib/gitlab/experimentation_spec.rb @@ -13,11 +13,8 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do :invite_members_version_a, :invite_members_version_b, :invite_members_empty_group_version_a, - :new_create_project_ui, :contact_sales_btn_in_app, :customize_homepage, - :invite_email, - :invitation_reminders, :group_only_trials, :default_to_issues_board ] @@ -29,127 +26,150 @@ RSpec.describe Gitlab::Experimentation::EXPERIMENTS do end end -RSpec.describe Gitlab::Experimentation, :snowplow do +RSpec.describe Gitlab::Experimentation do before do stub_const('Gitlab::Experimentation::EXPERIMENTS', { backwards_compatible_test_experiment: { - environment: environment, tracking_category: 'Team', use_backwards_compatible_subject_index: true }, test_experiment: { - environment: environment, tracking_category: 'Team' } }) Feature.enable_percentage_of_time(:backwards_compatible_test_experiment_experiment_percentage, enabled_percentage) Feature.enable_percentage_of_time(:test_experiment_experiment_percentage, enabled_percentage) + allow(Gitlab).to receive(:com?).and_return(true) end - let(:environment) { Rails.env.test? } let(:enabled_percentage) { 10 } - describe '.enabled?' do - subject { described_class.enabled?(:test_experiment) } + describe '.get_experiment' do + subject { described_class.get_experiment(:test_experiment) } - context 'feature toggle is enabled, we are on the right environment and we are selected' do - it { is_expected.to be_truthy } + context 'returns experiment' do + it { is_expected.to be_instance_of(Gitlab::Experimentation::Experiment) } + end + + context 'experiment is not defined' do + subject { described_class.get_experiment(:missing_experiment) } + + it { is_expected.to be_nil } + end + end + + describe '.active?' do + subject { described_class.active?(:test_experiment) } + + context 'feature toggle is enabled' do + it { is_expected.to eq(true) } end describe 'experiment is not defined' do it 'returns false' do - expect(described_class.enabled?(:missing_experiment)).to be_falsey + expect(described_class.active?(:missing_experiment)).to eq(false) end end describe 'experiment is disabled' do let(:enabled_percentage) { 0 } - it { is_expected.to be_falsey } + it { is_expected.to eq(false) } end + end - describe 'we are on the wrong environment' do - let(:environment) { ::Gitlab.com? } + describe '.in_experiment_group?' do + context 'with new index calculation' do + let(:enabled_percentage) { 50 } + let(:experiment_subject) { 'z' } # Zlib.crc32('test_experimentz') % 100 = 33 - it { is_expected.to be_falsey } + subject { described_class.in_experiment_group?(:test_experiment, subject: experiment_subject) } - it 'ensures the typically less expensive environment is checked before the more expensive call to database for Feature' do - expect_next_instance_of(described_class::Experiment) do |experiment| - expect(experiment).not_to receive(:enabled?) + context 'when experiment is active' do + context 'when subject is part of the experiment' do + it { is_expected.to eq(true) } end - subject - end - end - end + context 'when subject is not part of the experiment' do + let(:experiment_subject) { 'a' } # Zlib.crc32('test_experimenta') % 100 = 61 + + it { is_expected.to eq(false) } + end + + context 'when subject has a global_id' do + let(:experiment_subject) { double(:subject, to_global_id: 'z') } + + it { is_expected.to eq(true) } + end + + context 'when subject is nil' do + let(:experiment_subject) { nil } - describe '.enabled_for_value?' do - subject { described_class.enabled_for_value?(:test_experiment, experimentation_subject_index) } + it { is_expected.to eq(false) } + end - let(:experimentation_subject_index) { 9 } + context 'when subject is an empty string' do + let(:experiment_subject) { '' } - context 'experiment is disabled' do - before do - allow(described_class).to receive(:enabled?).and_return(false) + it { is_expected.to eq(false) } + end end - it { is_expected.to be_falsey } - end + context 'when experiment is not active' do + before do + allow(described_class).to receive(:active?).and_return(false) + end - context 'experiment is enabled' do - before do - allow(described_class).to receive(:enabled?).and_return(true) + it { is_expected.to eq(false) } end + end - it { is_expected.to be_truthy } + context 'with backwards compatible index calculation' do + let(:experiment_subject) { 'abcd' } # Digest::SHA1.hexdigest('abcd').hex % 100 = 7 - describe 'experimentation_subject_index' do - context 'experimentation_subject_index is not set' do - let(:experimentation_subject_index) { nil } + subject { described_class.in_experiment_group?(:backwards_compatible_test_experiment, subject: experiment_subject) } - it { is_expected.to be_falsey } + context 'when experiment is active' do + before do + allow(described_class).to receive(:active?).and_return(true) end - context 'experimentation_subject_index is an empty string' do - let(:experimentation_subject_index) { '' } - - it { is_expected.to be_falsey } + context 'when subject is part of the experiment' do + it { is_expected.to eq(true) } end - context 'experimentation_subject_index outside enabled ratio' do - let(:experimentation_subject_index) { 11 } + context 'when subject is not part of the experiment' do + let(:experiment_subject) { 'abc' } # Digest::SHA1.hexdigest('abc').hex % 100 = 17 - it { is_expected.to be_falsey } + it { is_expected.to eq(false) } end - end - end - end - describe '.enabled_for_attribute?' do - subject { described_class.enabled_for_attribute?(:test_experiment, attribute) } + context 'when subject has a global_id' do + let(:experiment_subject) { double(:subject, to_global_id: 'abcd') } - let(:attribute) { 'abcd' } # Digest::SHA1.hexdigest('abcd').hex % 100 = 7 + it { is_expected.to eq(true) } + end - context 'experiment is disabled' do - before do - allow(described_class).to receive(:enabled?).and_return(false) - end + context 'when subject is nil' do + let(:experiment_subject) { nil } - it { is_expected.to be false } - end + it { is_expected.to eq(false) } + end - context 'experiment is enabled' do - before do - allow(described_class).to receive(:enabled?).and_return(true) - end + context 'when subject is an empty string' do + let(:experiment_subject) { '' } - it { is_expected.to be true } + it { is_expected.to eq(false) } + end + end - context 'outside enabled ratio' do - let(:attribute) { 'abc' } # Digest::SHA1.hexdigest('abc').hex % 100 = 17 + context 'when experiment is not active' do + before do + allow(described_class).to receive(:active?).and_return(false) + end - it { is_expected.to be false } + it { is_expected.to eq(false) } end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 6dfa791f70b..c917945499c 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -929,7 +929,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end context 'with max_count' do - it 'returns the number of commits with path ' do + it 'returns the number of commits with path' do options = { ref: 'master', max_count: 5 } expect(repository.count_commits(options)).to eq(5) @@ -937,7 +937,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end context 'with path' do - it 'returns the number of commits with path ' do + it 'returns the number of commits with path' do options = { ref: 'master', path: 'encoding' } expect(repository.count_commits(options)).to eq(2) @@ -965,7 +965,7 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end context 'with max_count' do - it 'returns the number of commits with path ' do + it 'returns the number of commits with path' do options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 } expect(repository.count_commits(options)).to eq([1, 1]) @@ -1185,6 +1185,66 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do end end + describe '#find_changed_paths' do + let(:commit_1) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' } + let(:commit_2) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } + let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' } + let(:commit_1_files) do + [ + OpenStruct.new(status: :ADDED, path: "files/executables/ls"), + OpenStruct.new(status: :ADDED, path: "files/executables/touch"), + OpenStruct.new(status: :ADDED, path: "files/links/regex.rb"), + OpenStruct.new(status: :ADDED, path: "files/links/ruby-style-guide.md"), + OpenStruct.new(status: :ADDED, path: "files/links/touch"), + OpenStruct.new(status: :MODIFIED, path: ".gitmodules"), + OpenStruct.new(status: :ADDED, path: "deeper/nested/six"), + OpenStruct.new(status: :ADDED, path: "nested/six") + ] + end + + let(:commit_2_files) do + [OpenStruct.new(status: :ADDED, path: "bin/executable")] + end + + let(:commit_3_files) do + [ + OpenStruct.new(status: :MODIFIED, path: ".gitmodules"), + OpenStruct.new(status: :ADDED, path: "gitlab-shell") + ] + end + + it 'returns a list of paths' do + collection = repository.find_changed_paths([commit_1, commit_2, commit_3]) + + expect(collection).to be_a(Enumerable) + expect(collection.to_a).to eq(commit_1_files + commit_2_files + commit_3_files) + end + + it 'returns no paths when SHAs are invalid' do + collection = repository.find_changed_paths(['invalid', commit_1]) + + expect(collection).to be_a(Enumerable) + expect(collection.to_a).to be_empty + end + + it 'returns a list of paths even when containing a blank ref' do + collection = repository.find_changed_paths([nil, commit_1]) + + expect(collection).to be_a(Enumerable) + expect(collection.to_a).to eq(commit_1_files) + end + + it 'returns no paths when the commits are nil' do + expect_any_instance_of(Gitlab::GitalyClient::CommitService) + .not_to receive(:find_changed_paths) + + collection = repository.find_changed_paths([nil, nil]) + + expect(collection).to be_a(Enumerable) + expect(collection.to_a).to be_empty + end + end + describe "#ls_files" do let(:master_file_paths) { repository.ls_files("master") } let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") } diff --git a/spec/lib/gitlab/git_access_project_spec.rb b/spec/lib/gitlab/git_access_project_spec.rb index f80915b2be9..953b74cf1a9 100644 --- a/spec/lib/gitlab/git_access_project_spec.rb +++ b/spec/lib/gitlab/git_access_project_spec.rb @@ -9,6 +9,7 @@ RSpec.describe Gitlab::GitAccessProject do let(:actor) { user } let(:project_path) { project.path } let(:namespace_path) { project&.namespace&.path } + let(:repository_path) { "#{namespace_path}/#{project_path}.git" } let(:protocol) { 'ssh' } let(:authentication_abilities) { %i[read_project download_code push_code] } let(:changes) { Gitlab::GitAccess::ANY } @@ -17,7 +18,7 @@ RSpec.describe Gitlab::GitAccessProject do let(:access) do described_class.new(actor, container, protocol, authentication_abilities: authentication_abilities, - repository_path: project_path, namespace_path: namespace_path) + repository_path: repository_path) end describe '#check_namespace!' do @@ -103,6 +104,20 @@ RSpec.describe Gitlab::GitAccessProject do end end + context 'when namespace is blank' do + let(:repository_path) { 'project.git' } + + it_behaves_like 'no project is created' do + let(:raise_specific_error) { raise_namespace_not_found } + end + end + + context 'when namespace does not exist' do + let(:namespace_path) { 'unknown' } + + it_behaves_like 'no project is created' + end + context 'when user cannot create project in namespace' do let(:user2) { create(:user) } let(:namespace_path) { user2.namespace.path } diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 21607edbc32..780f4329bcc 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -10,8 +10,7 @@ RSpec.describe Gitlab::GitAccess do let(:actor) { user } let(:project) { create(:project, :repository) } - let(:project_path) { project&.path } - let(:namespace_path) { project&.namespace&.path } + let(:repository_path) { "#{project.full_path}.git" } let(:protocol) { 'ssh' } let(:authentication_abilities) { %i[read_project download_code push_code] } let(:redirected_path) { nil } @@ -210,10 +209,9 @@ RSpec.describe Gitlab::GitAccess do end end - context 'when the project is nil' do + context 'when the project does not exist' do let(:project) { nil } - let(:project_path) { "new-project" } - let(:namespace_path) { user.namespace.path } + let(:repository_path) { "#{user.namespace.path}/new-project.git" } it 'blocks push and pull with "not found"' do aggregate_failures do @@ -389,6 +387,108 @@ RSpec.describe Gitlab::GitAccess do end end + describe '#check_otp_session!' do + let_it_be(:user) { create(:user, :two_factor_via_otp)} + let_it_be(:key) { create(:key, user: user) } + let_it_be(:actor) { key } + + before do + project.add_developer(user) + stub_feature_flags(two_factor_for_cli: true) + end + + context 'with an OTP session', :clean_gitlab_redis_shared_state do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.set("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}", true) + end + end + + it 'allows push and pull access' do + aggregate_failures do + expect { push_access_check }.not_to raise_error + expect { pull_access_check }.not_to raise_error + end + end + end + + context 'without OTP session' do + it 'does not allow push or pull access' do + user = 'jane.doe' + host = 'fridge.ssh' + port = 42 + + stub_config( + gitlab_shell: { + ssh_user: user, + ssh_host: host, + ssh_port: port + } + ) + + error_message = "OTP verification is required to access the repository.\n\n"\ + " Use: ssh #{user}@#{host} -p #{port} 2fa_verify" + + aggregate_failures do + expect { push_access_check }.to raise_forbidden(error_message) + expect { pull_access_check }.to raise_forbidden(error_message) + end + end + + context 'when protocol is HTTP' do + let(:protocol) { 'http' } + + it 'allows push and pull access' do + aggregate_failures do + expect { push_access_check }.not_to raise_error + expect { pull_access_check }.not_to raise_error + end + end + end + + context 'when actor is not an SSH key' do + let(:deploy_key) { create(:deploy_key, user: user) } + let(:actor) { deploy_key } + + before do + deploy_key.deploy_keys_projects.create(project: project, can_push: true) + end + + it 'allows push and pull access' do + aggregate_failures do + expect { push_access_check }.not_to raise_error + expect { pull_access_check }.not_to raise_error + end + end + end + + context 'when 2FA is not enabled for the user' do + let(:user) { create(:user)} + let(:actor) { create(:key, user: user) } + + it 'allows push and pull access' do + aggregate_failures do + expect { push_access_check }.not_to raise_error + expect { pull_access_check }.not_to raise_error + end + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(two_factor_for_cli: false) + end + + it 'allows push and pull access' do + aggregate_failures do + expect { push_access_check }.not_to raise_error + expect { pull_access_check }.not_to raise_error + end + end + end + end + end + describe '#check_db_accessibility!' do context 'when in a read-only GitLab instance' do before do @@ -452,9 +552,8 @@ RSpec.describe Gitlab::GitAccess do context 'when project is public' do let(:public_project) { create(:project, :public, :repository) } - let(:project_path) { public_project.path } - let(:namespace_path) { public_project.namespace.path } - let(:access) { access_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], repository_path: project_path, namespace_path: namespace_path) } + let(:repository_path) { "#{public_project.full_path}.git" } + let(:access) { access_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], repository_path: repository_path) } context 'when repository is enabled' do it 'give access to download code' do @@ -1169,7 +1268,7 @@ RSpec.describe Gitlab::GitAccess do def access access_class.new(actor, project, protocol, authentication_abilities: authentication_abilities, - namespace_path: namespace_path, repository_path: project_path, + repository_path: repository_path, redirected_path: redirected_path, auth_result_type: auth_result_type) end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index b09bd9dff1b..157c2393ce1 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -145,6 +145,31 @@ RSpec.describe Gitlab::GitalyClient::CommitService do end end + describe '#find_changed_paths' do + let(:commits) { %w[1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 cfe32cf61b73a0d5e9f13e774abde7ff789b1660] } + + it 'sends an RPC request and returns the stats' do + request = Gitaly::FindChangedPathsRequest.new(repository: repository_message, + commits: commits) + + changed_paths_response = Gitaly::FindChangedPathsResponse.new( + paths: [{ + path: "app/assets/javascripts/boards/components/project_select.vue", + status: :MODIFIED + }]) + + expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:find_changed_paths) + .with(request, kind_of(Hash)).and_return([changed_paths_response]) + + returned_value = described_class.new(repository).find_changed_paths(commits) + + mapped_returned_value = returned_value.map(&:to_h) + mapped_expected_value = changed_paths_response.paths.map(&:to_h) + + expect(mapped_returned_value).to eq(mapped_expected_value) + end + end + describe '#tree_entries' do let(:path) { '/' } @@ -357,7 +382,7 @@ RSpec.describe Gitlab::GitalyClient::CommitService do end it 'sends an RPC request with the correct payload' do - expect(client.commits_by_message(query, options)).to match_array(wrap_commits(commits)) + expect(client.commits_by_message(query, **options)).to match_array(wrap_commits(commits)) end end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index 16dd2bbee6d..7fcb11c4dfd 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -53,7 +53,7 @@ RSpec.describe Gitlab::GitalyClient do describe '.filesystem_id_from_disk' do it 'catches errors' do [Errno::ENOENT, Errno::EACCES, JSON::ParserError].each do |error| - allow(File).to receive(:read).with(described_class.storage_metadata_file_path('default')).and_raise(error) + stub_file_read(described_class.storage_metadata_file_path('default'), error: error) expect(described_class.filesystem_id_from_disk('default')).to be_nil end diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index bc734644d29..4000e0b2611 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -28,6 +28,17 @@ RSpec.describe Gitlab::GithubImport::Client do end end + describe '#pull_request_reviews' do + it 'returns the pull request reviews' do + client = described_class.new('foo') + + expect(client.octokit).to receive(:pull_request_reviews).with('foo/bar', 999) + expect(client).to receive(:with_rate_limit).and_yield + + client.pull_request_reviews('foo/bar', 999) + end + end + describe '#repository' do it 'returns the details of a repository' do client = described_class.new('foo') @@ -39,6 +50,17 @@ RSpec.describe Gitlab::GithubImport::Client do end end + describe '#pull_request' do + it 'returns the details of a pull_request' do + client = described_class.new('foo') + + expect(client.octokit).to receive(:pull_request).with('foo/bar', 999) + expect(client).to receive(:with_rate_limit).and_yield + + client.pull_request('foo/bar', 999) + end + end + describe '#labels' do it 'returns the labels' do client = described_class.new('foo') @@ -478,7 +500,7 @@ RSpec.describe Gitlab::GithubImport::Client do it 'searches for repositories based on name' do expected_search_query = 'test in:name is:public,private user:user repo:repo1 repo:repo2 org:org1 org:org2' - expect(client).to receive(:each_page).with(:search_repositories, expected_search_query) + expect(client.octokit).to receive(:search_repositories).with(expected_search_query, {}) client.search_repos_by_name('test') end diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb index 6188ba8ec3f..8ee534734f0 100644 --- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb @@ -49,6 +49,57 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do importer.execute end end + + context 'when LFS list download fails' do + it 'rescues and logs the known exceptions' do + exception = StandardError.new('Invalid Project URL') + importer = described_class.new(project, client, parallel: false) + + expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service| + expect(service) + .to receive(:execute) + .and_raise(exception) + end + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger) + .to receive(:error) + .with( + message: 'importer failed', + import_source: :github, + project_id: project.id, + parallel: false, + importer: 'Gitlab::GithubImport::Importer::LfsObjectImporter', + 'error.message': 'Invalid Project URL' + ) + end + + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with( + exception, + import_source: :github, + parallel: false, + project_id: project.id, + importer: 'Gitlab::GithubImport::Importer::LfsObjectImporter' + ).and_call_original + + importer.execute + end + + it 'raises and logs the unknown exceptions' do + exception = Exception.new('Really bad news') + importer = described_class.new(project, client, parallel: false) + + expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service| + expect(service) + .to receive(:execute) + .and_raise(exception) + end + + expect { importer.execute }.to raise_error(exception) + end + end end describe '#sequential_import' do @@ -56,18 +107,16 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do importer = described_class.new(project, client, parallel: false) lfs_object_importer = double(:lfs_object_importer) - allow(importer) - .to receive(:each_object_to_import) - .and_yield(lfs_download_object) + expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service| + expect(service).to receive(:execute).and_return([lfs_download_object]) + end expect(Gitlab::GithubImport::Importer::LfsObjectImporter) - .to receive(:new) - .with( + .to receive(:new).with( an_instance_of(Gitlab::GithubImport::Representation::LfsObject), project, client - ) - .and_return(lfs_object_importer) + ).and_return(lfs_object_importer) expect(lfs_object_importer).to receive(:execute) @@ -79,9 +128,9 @@ RSpec.describe Gitlab::GithubImport::Importer::LfsObjectsImporter do it 'imports each lfs object in parallel' do importer = described_class.new(project, client) - allow(importer) - .to receive(:each_object_to_import) - .and_yield(lfs_download_object) + expect_next_instance_of(Projects::LfsPointers::LfsObjectDownloadListService) do |service| + expect(service).to receive(:execute).and_return([lfs_download_object]) + end expect(Gitlab::GithubImport::ImportLfsObjectWorker) .to receive(:perform_async) diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 46850618945..c7388314253 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -43,7 +43,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla describe '#execute' do it 'imports the pull request' do - mr = double(:merge_request, id: 10) + mr = double(:merge_request, id: 10, merged?: false) expect(importer) .to receive(:create_merge_request) diff --git a/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb new file mode 100644 index 00000000000..2999dc5bb41 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/pull_request_merged_by_importer_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :clean_gitlab_redis_cache do + let_it_be(:merge_request) { create(:merged_merge_request) } + let(:project) { merge_request.project } + let(:created_at) { Time.new(2017, 1, 1, 12, 00).utc } + let(:client_double) { double(user: double(id: 999, login: 'merger', email: 'merger@email.com')) } + + let(:pull_request) do + instance_double( + Gitlab::GithubImport::Representation::PullRequest, + iid: merge_request.iid, + created_at: created_at, + merged_by: double(id: 999, login: 'merger') + ) + end + + subject { described_class.new(pull_request, project, client_double) } + + it 'assigns the merged by user when mapped' do + merge_user = create(:user, email: 'merger@email.com') + + subject.execute + + expect(merge_request.metrics.reload.merged_by).to eq(merge_user) + end + + it 'adds a note referencing the merger user when the user cannot be mapped' do + expect { subject.execute } + .to change(Note, :count).by(1) + .and not_change(merge_request, :updated_at) + + last_note = merge_request.notes.last + + expect(last_note.note).to eq("*Merged by: merger*") + expect(last_note.created_at).to eq(created_at) + expect(last_note.author).to eq(project.creator) + end +end diff --git a/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb new file mode 100644 index 00000000000..b2f993ac47c --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/pull_request_review_importer_spec.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::PullRequestReviewImporter, :clean_gitlab_redis_cache do + using RSpec::Parameterized::TableSyntax + + let_it_be(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:client_double) { double(user: double(id: 999, login: 'author', email: 'author@email.com')) } + let(:submitted_at) { Time.new(2017, 1, 1, 12, 00).utc } + + subject { described_class.new(review, project, client_double) } + + context 'when the review author can be mapped to a gitlab user' do + let_it_be(:author) { create(:user, email: 'author@email.com') } + + context 'when the review has no note text' do + context 'when the review is "APPROVED"' do + let(:review) { create_review(type: 'APPROVED', note: '') } + + it 'creates a note for the review' do + expect { subject.execute }.to change(Note, :count) + + last_note = merge_request.notes.last + expect(last_note.note).to eq('approved this merge request') + expect(last_note.author).to eq(author) + expect(last_note.created_at).to eq(submitted_at) + expect(last_note.system_note_metadata.action).to eq('approved') + + expect(merge_request.approved_by_users.reload).to include(author) + expect(merge_request.approvals.last.created_at).to eq(submitted_at) + end + end + + context 'when the review is "COMMENTED"' do + let(:review) { create_review(type: 'COMMENTED', note: '') } + + it 'creates a note for the review' do + expect { subject.execute }.not_to change(Note, :count) + end + end + + context 'when the review is "CHANGES_REQUESTED"' do + let(:review) { create_review(type: 'CHANGES_REQUESTED', note: '') } + + it 'creates a note for the review' do + expect { subject.execute }.not_to change(Note, :count) + end + end + end + + context 'when the review has a note text' do + context 'when the review is "APPROVED"' do + let(:review) { create_review(type: 'APPROVED') } + + it 'creates a note for the review' do + expect { subject.execute } + .to change(Note, :count).by(2) + .and change(Approval, :count).by(1) + + note = merge_request.notes.where(system: false).last + expect(note.note).to eq("**Review:** Approved\n\nnote") + expect(note.author).to eq(author) + expect(note.created_at).to eq(submitted_at) + + system_note = merge_request.notes.where(system: true).last + expect(system_note.note).to eq('approved this merge request') + expect(system_note.author).to eq(author) + expect(system_note.created_at).to eq(submitted_at) + expect(system_note.system_note_metadata.action).to eq('approved') + + expect(merge_request.approved_by_users.reload).to include(author) + expect(merge_request.approvals.last.created_at).to eq(submitted_at) + end + end + + context 'when the review is "COMMENTED"' do + let(:review) { create_review(type: 'COMMENTED') } + + it 'creates a note for the review' do + expect { subject.execute } + .to change(Note, :count).by(1) + .and not_change(Approval, :count) + + last_note = merge_request.notes.last + + expect(last_note.note).to eq("**Review:** Commented\n\nnote") + expect(last_note.author).to eq(author) + expect(last_note.created_at).to eq(submitted_at) + end + end + + context 'when the review is "CHANGES_REQUESTED"' do + let(:review) { create_review(type: 'CHANGES_REQUESTED') } + + it 'creates a note for the review' do + expect { subject.execute } + .to change(Note, :count).by(1) + .and not_change(Approval, :count) + + last_note = merge_request.notes.last + + expect(last_note.note).to eq("**Review:** Changes requested\n\nnote") + expect(last_note.author).to eq(author) + expect(last_note.created_at).to eq(submitted_at) + end + end + end + end + + context 'when the review author cannot be mapped to a gitlab user' do + context 'when the review has no note text' do + context 'when the review is "APPROVED"' do + let(:review) { create_review(type: 'APPROVED', note: '') } + + it 'creates a note for the review with *Approved by by<author>*' do + expect { subject.execute } + .to change(Note, :count).by(1) + + last_note = merge_request.notes.last + expect(last_note.note).to eq("*Created by author*\n\n**Review:** Approved") + expect(last_note.author).to eq(project.creator) + expect(last_note.created_at).to eq(submitted_at) + end + end + + context 'when the review is "COMMENTED"' do + let(:review) { create_review(type: 'COMMENTED', note: '') } + + it 'creates a note for the review with *Commented by<author>*' do + expect { subject.execute }.not_to change(Note, :count) + end + end + + context 'when the review is "CHANGES_REQUESTED"' do + let(:review) { create_review(type: 'CHANGES_REQUESTED', note: '') } + + it 'creates a note for the review with *Changes requested by <author>*' do + expect { subject.execute }.not_to change(Note, :count) + end + end + end + + context 'when the review has a note text' do + context 'when the review is "APPROVED"' do + let(:review) { create_review(type: 'APPROVED') } + + it 'creates a note for the review with *Approved by by<author>*' do + expect { subject.execute } + .to change(Note, :count).by(1) + + last_note = merge_request.notes.last + + expect(last_note.note).to eq("*Created by author*\n\n**Review:** Approved\n\nnote") + expect(last_note.author).to eq(project.creator) + expect(last_note.created_at).to eq(submitted_at) + end + end + + context 'when the review is "COMMENTED"' do + let(:review) { create_review(type: 'COMMENTED') } + + it 'creates a note for the review with *Commented by<author>*' do + expect { subject.execute } + .to change(Note, :count).by(1) + + last_note = merge_request.notes.last + + expect(last_note.note).to eq("*Created by author*\n\n**Review:** Commented\n\nnote") + expect(last_note.author).to eq(project.creator) + expect(last_note.created_at).to eq(submitted_at) + end + end + + context 'when the review is "CHANGES_REQUESTED"' do + let(:review) { create_review(type: 'CHANGES_REQUESTED') } + + it 'creates a note for the review with *Changes requested by <author>*' do + expect { subject.execute } + .to change(Note, :count).by(1) + + last_note = merge_request.notes.last + + expect(last_note.note).to eq("*Created by author*\n\n**Review:** Changes requested\n\nnote") + expect(last_note.author).to eq(project.creator) + expect(last_note.created_at).to eq(submitted_at) + end + end + end + end + + def create_review(type:, note: 'note') + Gitlab::GithubImport::Representation::PullRequestReview.from_json_hash( + merge_request_id: merge_request.id, + review_type: type, + note: note, + submitted_at: submitted_at.to_s, + author: { id: 999, login: 'author' } + ) + end +end diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb index 0835c6155b9..8a7867f3841 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb @@ -29,6 +29,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsImporter do milestone: double(:milestone, number: 4), user: double(:user, id: 4, login: 'alice'), assignee: double(:user, id: 4, login: 'alice'), + merged_by: double(:user, id: 4, login: 'alice'), created_at: 1.second.ago, updated_at: 1.second.ago, merged_at: 1.second.ago diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb new file mode 100644 index 00000000000..b859cc727a6 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/pull_requests_merged_by_importer_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do + let(:client) { double } + let(:project) { create(:project, import_source: 'http://somegithub.com') } + + subject { described_class.new(project, client) } + + it { is_expected.to include_module(Gitlab::GithubImport::ParallelScheduling) } + + describe '#representation_class' do + it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::PullRequest) } + end + + describe '#importer_class' do + it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequestMergedByImporter) } + end + + describe '#collection_method' do + it { expect(subject.collection_method).to eq(:pull_requests_merged_by) } + end + + describe '#id_for_already_imported_cache' do + it { expect(subject.id_for_already_imported_cache(double(number: 1))).to eq(1) } + end + + describe '#each_object_to_import' do + it 'fetchs the merged pull requests data' do + pull_request = double + create( + :merged_merge_request, + iid: 999, + source_project: project, + target_project: project + ) + + allow(client) + .to receive(:pull_request) + .with('http://somegithub.com', 999) + .and_return(pull_request) + + expect { |b| subject.each_object_to_import(&b) }.to yield_with_args(pull_request) + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb new file mode 100644 index 00000000000..5e2302f9662 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/pull_requests_reviews_importer_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::PullRequestsReviewsImporter do + let(:client) { double } + let(:project) { create(:project, import_source: 'github/repo') } + + subject { described_class.new(project, client) } + + it { is_expected.to include_module(Gitlab::GithubImport::ParallelScheduling) } + + describe '#representation_class' do + it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::PullRequestReview) } + end + + describe '#importer_class' do + it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequestReviewImporter) } + end + + describe '#collection_method' do + it { expect(subject.collection_method).to eq(:pull_request_reviews) } + end + + describe '#id_for_already_imported_cache' do + it { expect(subject.id_for_already_imported_cache(double(github_id: 1))).to eq(1) } + end + + describe '#each_object_to_import' do + it 'fetchs the merged pull requests data' do + merge_request = create(:merge_request, source_project: project) + review = double + + expect(review) + .to receive(:merge_request_id=) + .with(merge_request.id) + + allow(client) + .to receive(:pull_request_reviews) + .with('github/repo', merge_request.iid) + .and_return([review]) + + expect { |b| subject.each_object_to_import(&b) }.to yield_with_args(review) + end + end +end diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb index 578743be96b..1e31cd2f007 100644 --- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb +++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb @@ -7,6 +7,10 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do Class.new do include(Gitlab::GithubImport::ParallelScheduling) + def importer_class + Class + end + def collection_method :issues end @@ -63,6 +67,82 @@ RSpec.describe Gitlab::GithubImport::ParallelScheduling do importer.execute end + + it 'logs the the process' do + importer = importer_class.new(project, client, parallel: false) + + expect(importer) + .to receive(:sequential_import) + .and_return([]) + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger) + .to receive(:info) + .with( + message: 'starting importer', + import_source: :github, + parallel: false, + project_id: project.id, + importer: 'Class' + ) + expect(logger) + .to receive(:info) + .with( + message: 'importer finished', + import_source: :github, + parallel: false, + project_id: project.id, + importer: 'Class' + ) + end + + importer.execute + end + + it 'logs the error when it fails' do + exception = StandardError.new('some error') + + importer = importer_class.new(project, client, parallel: false) + + expect(importer) + .to receive(:sequential_import) + .and_raise(exception) + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger) + .to receive(:info) + .with( + message: 'starting importer', + import_source: :github, + parallel: false, + project_id: project.id, + importer: 'Class' + ) + expect(logger) + .to receive(:error) + .with( + message: 'importer failed', + import_source: :github, + project_id: project.id, + parallel: false, + importer: 'Class', + 'error.message': 'some error' + ) + end + + expect(Gitlab::ErrorTracking) + .to receive(:track_exception) + .with( + exception, + import_source: :github, + parallel: false, + project_id: project.id, + importer: 'Class' + ) + .and_call_original + + expect { importer.execute }.to raise_error(exception) + end end describe '#sequential_import' do diff --git a/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb new file mode 100644 index 00000000000..f9763455468 --- /dev/null +++ b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do + let(:submitted_at) { Time.new(2017, 1, 1, 12, 00).utc } + + shared_examples 'a PullRequest review' do + it 'returns an instance of PullRequest' do + expect(review).to be_an_instance_of(described_class) + expect(review.author).to be_an_instance_of(Gitlab::GithubImport::Representation::User) + expect(review.author.id).to eq(4) + expect(review.author.login).to eq('alice') + expect(review.note).to eq('note') + expect(review.review_type).to eq('APPROVED') + expect(review.submitted_at).to eq(submitted_at) + expect(review.github_id).to eq(999) + expect(review.merge_request_id).to eq(42) + end + end + + describe '.from_api_response' do + let(:response) do + double( + :response, + id: 999, + merge_request_id: 42, + body: 'note', + state: 'APPROVED', + user: double(:user, id: 4, login: 'alice'), + submitted_at: submitted_at + ) + end + + it_behaves_like 'a PullRequest review' do + let(:review) { described_class.from_api_response(response) } + end + + it 'does not set the user if the response did not include a user' do + allow(response) + .to receive(:user) + .and_return(nil) + + review = described_class.from_api_response(response) + + expect(review.author).to be_nil + end + end + + describe '.from_json_hash' do + let(:hash) do + { + 'github_id' => 999, + 'merge_request_id' => 42, + 'note' => 'note', + 'review_type' => 'APPROVED', + 'author' => { 'id' => 4, 'login' => 'alice' }, + 'submitted_at' => submitted_at.to_s + } + end + + it_behaves_like 'a PullRequest review' do + let(:review) { described_class.from_json_hash(hash) } + end + + it 'does not set the user if the response did not include a user' do + review = described_class.from_json_hash(hash.except('author')) + + expect(review.author).to be_nil + end + end +end diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb index 370eac1d993..27a82951b01 100644 --- a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb +++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb @@ -115,6 +115,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequest do milestone: double(:milestone, number: 4), user: double(:user, id: 4, login: 'alice'), assignee: double(:user, id: 4, login: 'alice'), + merged_by: double(:user, id: 4, login: 'alice'), created_at: created_at, updated_at: updated_at, merged_at: merged_at diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb deleted file mode 100644 index 402d2169432..00000000000 --- a/spec/lib/gitlab/google_code_import/client_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe Gitlab::GoogleCodeImport::Client do - let(:raw_data) { Gitlab::Json.parse(fixture_file("GoogleCodeProjectHosting.json")) } - - subject { described_class.new(raw_data) } - - describe "#valid?" do - context "when the data is valid" do - it "returns true" do - expect(subject).to be_valid - end - end - - context "when the data is invalid" do - let(:raw_data) { "No clue" } - - it "returns true" do - expect(subject).not_to be_valid - end - end - end - - describe "#repos" do - it "returns only Git repositories" do - expect(subject.repos.length).to eq(1) - expect(subject.incompatible_repos.length).to eq(1) - end - end - - describe "#repo" do - it "returns the referenced repository" do - expect(subject.repo("tint2").name).to eq("tint2") - end - end -end diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb deleted file mode 100644 index a22e80ae1c0..00000000000 --- a/spec/lib/gitlab/google_code_import/importer_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -RSpec.describe Gitlab::GoogleCodeImport::Importer do - let(:mapped_user) { create(:user, username: "thilo123") } - let(:raw_data) { Gitlab::Json.parse(fixture_file("GoogleCodeProjectHosting.json")) } - let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) } - let(:import_data) do - { - 'repo' => client.repo('tint2').raw_data, - 'user_map' => { 'thilo...' => "@#{mapped_user.username}" } - } - end - - let(:project) { create(:project) } - - subject { described_class.new(project) } - - before do - project.add_maintainer(project.creator) - project.create_import_data(data: import_data) - end - - describe "#execute" do - it "imports status labels" do - subject.execute - - %w(New NeedInfo Accepted Wishlist Started Fixed Invalid Duplicate WontFix Incomplete).each do |status| - expect(project.labels.find_by(name: "Status: #{status}")).not_to be_nil - end - end - - it "imports labels" do - subject.execute - - %w( - Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical - Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security - Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery - Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New - ).each do |label| - label = label.sub("-", ": ") - expect(project.labels.find_by(name: label)).not_to be_nil - end - end - - it "imports issues" do - subject.execute - - issue = project.issues.first - expect(issue).not_to be_nil - expect(issue.iid).to eq(169) - expect(issue.author).to eq(project.creator) - expect(issue.assignees).to eq([mapped_user]) - expect(issue.state).to eq("closed") - expect(issue.label_names).to include("Priority: Medium") - expect(issue.label_names).to include("Status: Fixed") - expect(issue.label_names).to include("Type: Enhancement") - expect(issue.title).to eq("Scrolling through tasks") - expect(issue.state).to eq("closed") - expect(issue.description).to include("schattenpr\\.\\.\\.") - expect(issue.description).to include("November 18, 2009 00:20") - expect(issue.description).to include("Google Code") - expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel (like in fluxbox).') - expect(issue.description).to include('Patch is attached that adds two new mouse-actions (next_task+prev_task)') - expect(issue.description).to include('that can be used for exactly that purpose.') - expect(issue.description).to include('all the best!') - expect(issue.description).to include('[tint2_task_scrolling.diff](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff)') - expect(issue.description).to include('![screenshot.png](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/screenshot.png)') - expect(issue.description).to include('![screenshot1.PNG](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/screenshot1.PNG)') - end - - it "imports issue comments" do - subject.execute - - note = project.issues.first.notes.first - expect(note).not_to be_nil - expect(note.note).to include("Comment 1") - expect(note.note).to include("@#{mapped_user.username}") - expect(note.note).to include("November 18, 2009 05:14") - expect(note.note).to include("applied, thanks.") - expect(note.note).to include("Status: Fixed") - expect(note.note).to include("~~Type: Defect~~") - expect(note.note).to include("Type: Enhancement") - end - end -end diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb deleted file mode 100644 index cfebe57aed3..00000000000 --- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::GoogleCodeImport::ProjectCreator do - let(:user) { create(:user) } - let(:repo) do - Gitlab::GoogleCodeImport::Repository.new( - "name" => 'vim', - "summary" => 'VI Improved', - "repositoryUrls" => ["https://vim.googlecode.com/git/"] - ) - end - - let(:namespace) { create(:group) } - - before do - namespace.add_owner(user) - end - - it 'creates project' do - expect_next_instance_of(Project) do |project| - expect(project).to receive(:add_import_job) - end - - project_creator = described_class.new(repo, namespace, user) - project = project_creator.execute - - expect(project.import_url).to eq("https://vim.googlecode.com/git/") - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) - end -end diff --git a/spec/lib/gitlab/graphql/docs/renderer_spec.rb b/spec/lib/gitlab/graphql/docs/renderer_spec.rb index d1be962a4f8..064e0c6828b 100644 --- a/spec/lib/gitlab/graphql/docs/renderer_spec.rb +++ b/spec/lib/gitlab/graphql/docs/renderer_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do Class.new(Types::BaseObject) do graphql_name 'ArrayTest' - field :foo, [GraphQL::STRING_TYPE], null: false, description: 'A description' + field :foo, [GraphQL::STRING_TYPE], null: false, description: 'A description.' end end @@ -40,7 +40,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do | Field | Type | Description | | ----- | ---- | ----------- | - | `foo` | String! => Array | A description | + | `foo` | String! => Array | A description. | DOC is_expected.to include(expectation) @@ -52,8 +52,8 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do Class.new(Types::BaseObject) do graphql_name 'OrderingTest' - field :foo, GraphQL::STRING_TYPE, null: false, description: 'A description of foo field' - field :bar, GraphQL::STRING_TYPE, null: false, description: 'A description of bar field' + field :foo, GraphQL::STRING_TYPE, null: false, description: 'A description of foo field.' + field :bar, GraphQL::STRING_TYPE, null: false, description: 'A description of bar field.' end end @@ -63,8 +63,8 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do | Field | Type | Description | | ----- | ---- | ----------- | - | `bar` | String! | A description of bar field | - | `foo` | String! | A description of foo field | + | `bar` | String! | A description of bar field. | + | `foo` | String! | A description of foo field. | DOC is_expected.to include(expectation) @@ -76,7 +76,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do Class.new(Types::BaseObject) do graphql_name 'DeprecatedTest' - field :foo, GraphQL::STRING_TYPE, null: false, deprecated: { reason: 'This is deprecated', milestone: '1.10' }, description: 'A description' + field :foo, GraphQL::STRING_TYPE, null: false, deprecated: { reason: 'This is deprecated', milestone: '1.10' }, description: 'A description.' end end @@ -86,7 +86,7 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do | Field | Type | Description | | ----- | ---- | ----------- | - | `foo` **{warning-solid}** | String! | **Deprecated:** This is deprecated. Deprecated in 1.10 | + | `foo` **{warning-solid}** | String! | **Deprecated:** This is deprecated. Deprecated in 1.10. | DOC is_expected.to include(expectation) @@ -98,14 +98,14 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do enum_type = Class.new(Types::BaseEnum) do graphql_name 'MyEnum' - value 'BAZ', description: 'A description of BAZ' - value 'BAR', description: 'A description of BAR', deprecated: { reason: 'This is deprecated', milestone: '1.10' } + value 'BAZ', description: 'A description of BAZ.' + value 'BAR', description: 'A description of BAR.', deprecated: { reason: 'This is deprecated', milestone: '1.10' } end Class.new(Types::BaseObject) do graphql_name 'EnumTest' - field :foo, enum_type, null: false, description: 'A description of foo field' + field :foo, enum_type, null: false, description: 'A description of foo field.' end end @@ -115,8 +115,8 @@ RSpec.describe Gitlab::Graphql::Docs::Renderer do | Value | Description | | ----- | ----------- | - | `BAR` **{warning-solid}** | **Deprecated:** This is deprecated. Deprecated in 1.10 | - | `BAZ` | A description of BAZ | + | `BAR` **{warning-solid}** | **Deprecated:** This is deprecated. Deprecated in 1.10. | + | `BAZ` | A description of BAZ. | DOC is_expected.to include(expectation) diff --git a/spec/lib/gitlab/graphql/markdown_field_spec.rb b/spec/lib/gitlab/graphql/markdown_field_spec.rb index 82090f992eb..0e36ea14ac3 100644 --- a/spec/lib/gitlab/graphql/markdown_field_spec.rb +++ b/spec/lib/gitlab/graphql/markdown_field_spec.rb @@ -22,6 +22,8 @@ RSpec.describe Gitlab::Graphql::MarkdownField do .to raise_error(expected_error) end + # TODO: remove as part of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536 + # so that until that time, the developer check is there it 'raises when passing a resolve block' do expect { class_with_markdown_field(:test_html, null: true, resolve: -> (_, _, _) { 'not really' } ) } .to raise_error(expected_error) diff --git a/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb new file mode 100644 index 00000000000..03cf53bb990 --- /dev/null +++ b/spec/lib/gitlab/graphql/pagination/array_connection_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Graphql::Pagination::ArrayConnection do + let(:nodes) { (1..10) } + + subject(:connection) { described_class.new(nodes, max_page_size: 100) } + + it_behaves_like 'a connection with collection methods' + + it_behaves_like 'a redactable connection' do + let(:unwanted) { 5 } + end +end diff --git a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb index 932bcd8cd92..d2475d1edb9 100644 --- a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb @@ -10,7 +10,13 @@ RSpec.describe Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection d let(:arguments) { {} } subject(:connection) do - described_class.new(all_nodes, { max_page_size: values.size }.merge(arguments)) + described_class.new(all_nodes, **{ max_page_size: values.size }.merge(arguments)) + end + + it_behaves_like 'a connection with collection methods' + + it_behaves_like 'a redactable connection' do + let(:unwanted) { 3 } end describe '#nodes' do diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index c8f368b15fc..0ac54a20fcc 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -10,17 +10,24 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) } subject(:connection) do - described_class.new(nodes, { context: context, max_page_size: 3 }.merge(arguments)) + described_class.new(nodes, **{ context: context, max_page_size: 3 }.merge(arguments)) end def encoded_cursor(node) - described_class.new(nodes, { context: context }).cursor_for(node) + described_class.new(nodes, context: context).cursor_for(node) end def decoded_cursor(cursor) Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor)) end + it_behaves_like 'a connection with collection methods' + + it_behaves_like 'a redactable connection' do + let_it_be(:projects) { create_list(:project, 2) } + let(:unwanted) { projects.second } + end + describe '#cursor_for' do let(:project) { create(:project) } let(:cursor) { connection.cursor_for(project) } diff --git a/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb index 86f35de94ed..1ca7c1c3c69 100644 --- a/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/offset_active_record_relation_connection_spec.rb @@ -6,4 +6,15 @@ RSpec.describe Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection it 'subclasses from GraphQL::Relay::RelationConnection' do expect(described_class.superclass).to eq GraphQL::Pagination::ActiveRecordRelationConnection end + + it_behaves_like 'a connection with collection methods' do + let(:connection) { described_class.new(Project.all) } + end + + it_behaves_like 'a redactable connection' do + let_it_be(:users) { create_list(:user, 2) } + + let(:connection) { described_class.new(User.all, max_page_size: 10) } + let(:unwanted) { users.second } + end end diff --git a/spec/lib/gitlab/graphql/timeout_spec.rb b/spec/lib/gitlab/graphql/timeout_spec.rb index 3669a89ba7c..999840019d2 100644 --- a/spec/lib/gitlab/graphql/timeout_spec.rb +++ b/spec/lib/gitlab/graphql/timeout_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::Timeout do - it 'inherits from ' do + it 'inherits from' do expect(described_class.superclass).to eq GraphQL::Schema::Timeout end diff --git a/spec/lib/gitlab/hook_data/group_member_builder_spec.rb b/spec/lib/gitlab/hook_data/group_member_builder_spec.rb new file mode 100644 index 00000000000..78c62fd23c7 --- /dev/null +++ b/spec/lib/gitlab/hook_data/group_member_builder_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HookData::GroupMemberBuilder do + let_it_be(:group) { create(:group) } + let_it_be(:group_member) { create(:group_member, :developer, group: group, expires_at: 1.day.from_now) } + + describe '#build' do + let(:data) { described_class.new(group_member).build(event) } + let(:event_name) { data[:event_name] } + let(:attributes) do + [ + :event_name, :created_at, :updated_at, :expires_at, :group_name, :group_path, + :group_id, :user_id, :user_username, :user_name, :user_email, :group_access + ] + end + + context 'data' do + shared_examples_for 'includes the required attributes' do + it 'includes the required attributes' do + expect(data).to include(*attributes) + + expect(data[:group_name]).to eq(group.name) + expect(data[:group_path]).to eq(group.path) + expect(data[:group_id]).to eq(group.id) + expect(data[:user_username]).to eq(group_member.user.username) + expect(data[:user_name]).to eq(group_member.user.name) + expect(data[:user_email]).to eq(group_member.user.email) + expect(data[:user_id]).to eq(group_member.user.id) + expect(data[:group_access]).to eq('Developer') + expect(data[:created_at]).to eq(group_member.created_at&.xmlschema) + expect(data[:updated_at]).to eq(group_member.updated_at&.xmlschema) + expect(data[:expires_at]).to eq(group_member.expires_at&.xmlschema) + end + end + + context 'on create' do + let(:event) { :create } + + it { expect(event_name).to eq('user_add_to_group') } + it_behaves_like 'includes the required attributes' + end + + context 'on update' do + let(:event) { :update } + + it { expect(event_name).to eq('user_update_for_group') } + it_behaves_like 'includes the required attributes' + end + + context 'on destroy' do + let(:event) { :destroy } + + it { expect(event_name).to eq('user_remove_from_group') } + it_behaves_like 'includes the required attributes' + end + end + end +end diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb index e04c0b49480..f2ee6bb72d9 100644 --- a/spec/lib/gitlab/i18n/po_linter_spec.rb +++ b/spec/lib/gitlab/i18n/po_linter_spec.rb @@ -6,7 +6,7 @@ require 'simple_po_parser' # Disabling this cop to allow for multi-language examples in comments # rubocop:disable Style/AsciiComments RSpec.describe Gitlab::I18n::PoLinter do - let(:linter) { described_class.new(po_path: po_path, html_todolist: {}) } + let(:linter) { described_class.new(po_path: po_path) } let(:po_path) { 'spec/fixtures/valid.po' } def fake_translation(msgid:, translation:, plural_id: nil, plurals: []) @@ -24,8 +24,7 @@ RSpec.describe Gitlab::I18n::PoLinter do Gitlab::I18n::TranslationEntry.new( entry_data: data, - nplurals: plurals.size + 1, - html_allowed: nil + nplurals: plurals.size + 1 ) end @@ -160,53 +159,6 @@ RSpec.describe Gitlab::I18n::PoLinter do ] end end - - context 'when an entry contains html on the todolist' do - subject(:linter) { described_class.new(po_path: po_path, html_todolist: todolist) } - - let(:po_path) { 'spec/fixtures/potential_html.po' } - let(:todolist) do - { - 'String with a legitimate < use' => { - 'plural_id' => 'String with lots of < > uses', - 'translations' => [ - 'Translated string with a legitimate < use', - 'Translated string with lots of < > uses' - ] - } - } - end - - it 'does not present an error' do - message_id = 'String with a legitimate < use' - - expect(errors[message_id]).to be_nil - end - end - - context 'when an entry on the html todolist has changed' do - subject(:linter) { described_class.new(po_path: po_path, html_todolist: todolist) } - - let(:po_path) { 'spec/fixtures/potential_html.po' } - let(:todolist) do - { - 'String with a legitimate < use' => { - 'plural_id' => 'String with lots of < > uses', - 'translations' => [ - 'Translated string with a different legitimate < use', - 'Translated string with lots of < > uses' - ] - } - } - end - - it 'presents an error for the changed component' do - message_id = 'String with a legitimate < use' - - expect(errors[message_id]) - .to include a_string_starting_with('translation contains < or >.') - end - end end describe '#parse_po' do @@ -276,8 +228,7 @@ RSpec.describe Gitlab::I18n::PoLinter do fake_entry = Gitlab::I18n::TranslationEntry.new( entry_data: { msgid: 'the singular', msgid_plural: 'the plural', 'msgstr[0]' => 'the singular' }, - nplurals: 2, - html_allowed: nil + nplurals: 2 ) errors = [] diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb index 2c95b0b0124..f05346d07d3 100644 --- a/spec/lib/gitlab/i18n/translation_entry_spec.rb +++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#singular_translation' do it 'returns the normal `msgstr` for translations without plural' do data = { msgid: 'Hello world', msgstr: 'Bonjour monde' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.singular_translation).to eq('Bonjour monde') end @@ -18,7 +18,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do 'msgstr[0]' => 'Bonjour monde', 'msgstr[1]' => 'Bonjour mondes' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.singular_translation).to eq('Bonjour monde') end @@ -27,7 +27,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#all_translations' do it 'returns all translations for singular translations' do data = { msgid: 'Hello world', msgstr: 'Bonjour monde' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.all_translations).to eq(['Bonjour monde']) end @@ -39,7 +39,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do 'msgstr[0]' => 'Bonjour monde', 'msgstr[1]' => 'Bonjour mondes' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.all_translations).to eq(['Bonjour monde', 'Bonjour mondes']) end @@ -52,7 +52,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do msgid_plural: 'Hello worlds', 'msgstr[0]' => 'Bonjour monde' } - entry = described_class.new(entry_data: data, nplurals: 1, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 1) expect(entry.plural_translations).to eq(['Bonjour monde']) end @@ -65,7 +65,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do 'msgstr[1]' => 'Bonjour mondes', 'msgstr[2]' => 'Bonjour tous les mondes' } - entry = described_class.new(entry_data: data, nplurals: 3, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 3) expect(entry.plural_translations).to eq(['Bonjour mondes', 'Bonjour tous les mondes']) end @@ -77,7 +77,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do msgid: 'hello world', msgstr: 'hello' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to have_singular_translation end @@ -89,7 +89,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do "msgstr[0]" => 'hello world', "msgstr[1]" => 'hello worlds' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to have_singular_translation end @@ -100,7 +100,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do msgid_plural: 'hello worlds', "msgstr[0]" => 'hello worlds' } - entry = described_class.new(entry_data: data, nplurals: 1, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 1) expect(entry).not_to have_singular_translation end @@ -109,7 +109,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#msgid_contains_newlines' do it 'is true when the msgid is an array' do data = { msgid: %w(hello world) } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.msgid_has_multiple_lines?).to be_truthy end @@ -118,7 +118,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#plural_id_contains_newlines' do it 'is true when the msgid is an array' do data = { msgid_plural: %w(hello world) } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.plural_id_has_multiple_lines?).to be_truthy end @@ -127,7 +127,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#translations_contain_newlines' do it 'is true when the msgid is an array' do data = { msgstr: %w(hello world) } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry.translations_have_multiple_lines?).to be_truthy end @@ -135,7 +135,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#contains_unescaped_chars' do let(:data) { { msgid: '' } } - let(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) } + let(:entry) { described_class.new(entry_data: data, nplurals: 2) } it 'is true when the msgid is an array' do string = '「100%確定」' @@ -177,7 +177,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#msgid_contains_unescaped_chars' do it 'is true when the msgid contains a `%`' do data = { msgid: '「100%確定」' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to receive(:contains_unescaped_chars?).and_call_original expect(entry.msgid_contains_unescaped_chars?).to be_truthy @@ -187,7 +187,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#plural_id_contains_unescaped_chars' do it 'is true when the plural msgid contains a `%`' do data = { msgid_plural: '「100%確定」' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to receive(:contains_unescaped_chars?).and_call_original expect(entry.plural_id_contains_unescaped_chars?).to be_truthy @@ -197,7 +197,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do describe '#translations_contain_unescaped_chars' do it 'is true when the translation contains a `%`' do data = { msgstr: '「100%確定」' } - entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) + entry = described_class.new(entry_data: data, nplurals: 2) expect(entry).to receive(:contains_unescaped_chars?).and_call_original expect(entry.translations_contain_unescaped_chars?).to be_truthy @@ -205,7 +205,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do end describe '#msgid_contains_potential_html?' do - subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) } + subject(:entry) { described_class.new(entry_data: data, nplurals: 2) } context 'when there are no angle brackets in the msgid' do let(:data) { { msgid: 'String with no brackets' } } @@ -225,7 +225,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do end describe '#plural_id_contains_potential_html?' do - subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) } + subject(:entry) { described_class.new(entry_data: data, nplurals: 2) } context 'when there are no angle brackets in the plural_id' do let(:data) { { msgid_plural: 'String with no brackets' } } @@ -245,7 +245,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do end describe '#translations_contain_potential_html?' do - subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) } + subject(:entry) { described_class.new(entry_data: data, nplurals: 2) } context 'when there are no angle brackets in the translations' do let(:data) { { msgstr: 'This string has no angle brackets' } } @@ -263,78 +263,4 @@ RSpec.describe Gitlab::I18n::TranslationEntry do end end end - - describe '#msgid_html_allowed?' do - subject(:entry) do - described_class.new(entry_data: { msgid: 'String with a <strong>' }, nplurals: 2, html_allowed: html_todo) - end - - context 'when the html in the string is in the todolist' do - let(:html_todo) { { 'plural_id' => nil, 'translations' => [] } } - - it 'returns true' do - expect(entry.msgid_html_allowed?).to be true - end - end - - context 'when the html in the string is not in the todolist' do - let(:html_todo) { nil } - - it 'returns false' do - expect(entry.msgid_html_allowed?).to be false - end - end - end - - describe '#plural_id_html_allowed?' do - subject(:entry) do - described_class.new(entry_data: { msgid_plural: 'String with many <strong>' }, nplurals: 2, html_allowed: html_todo) - end - - context 'when the html in the string is in the todolist' do - let(:html_todo) { { 'plural_id' => 'String with many <strong>', 'translations' => [] } } - - it 'returns true' do - expect(entry.plural_id_html_allowed?).to be true - end - end - - context 'when the html in the string is not in the todolist' do - let(:html_todo) { { 'plural_id' => 'String with some <strong>', 'translations' => [] } } - - it 'returns false' do - expect(entry.plural_id_html_allowed?).to be false - end - end - end - - describe '#translations_html_allowed?' do - subject(:entry) do - described_class.new(entry_data: { msgstr: 'String with a <strong>' }, nplurals: 2, html_allowed: html_todo) - end - - context 'when the html in the string is in the todolist' do - let(:html_todo) { { 'plural_id' => nil, 'translations' => ['String with a <strong>'] } } - - it 'returns true' do - expect(entry.translations_html_allowed?).to be true - end - end - - context 'when the html in the string is not in the todolist' do - let(:html_todo) { { 'plural_id' => nil, 'translations' => ['String with a different <strong>'] } } - - it 'returns false' do - expect(entry.translations_html_allowed?).to be false - end - end - - context 'when the todolist only has the msgid' do - let(:html_todo) { { 'plural_id' => nil, 'translations' => nil } } - - it 'returns false' do - expect(entry.translations_html_allowed?).to be false - end - end - end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 38fe2781331..fba32ae0673 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -28,6 +28,7 @@ issues: - events - merge_requests_closing_issues - metrics +- metric_images - timelogs - issuable_severity - issuable_sla @@ -85,6 +86,7 @@ label: - issues - merge_requests - priorities +- epic_board_labels milestone: - group - project @@ -105,6 +107,7 @@ snippets: - user_mentions - snippet_repository - statistics +- repository_storage_moves releases: - author - project @@ -349,6 +352,7 @@ project: - services - campfire_service - confluence_service +- datadog_service - discord_service - drone_ci_service - emails_on_push_service @@ -540,6 +544,7 @@ project: - daily_build_group_report_results - jira_imports - compliance_framework_setting +- compliance_management_frameworks - metrics_users_starred_dashboards - alert_management_alerts - repository_storage_moves @@ -548,10 +553,13 @@ project: - build_report_results - vulnerability_statistic - vulnerability_historical_statistics +- vulnerability_remediations - product_analytics_events - pipeline_artifacts - terraform_states - alert_management_http_integrations +- exported_protected_branches +- incident_management_oncall_schedules award_emoji: - awardable - user @@ -639,6 +647,7 @@ boards: - lists - destroyable_lists - milestone +- iteration - board_labels - board_assignee - assignee @@ -648,6 +657,7 @@ boards: lists: - user - milestone +- iteration - board - label - list_user_preferences diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb index 2eb983cc050..2794acb8980 100644 --- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb @@ -75,12 +75,31 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer do before do setup_import_export_config('group_exports/child_with_no_parent') + end + + it 'captures import failures when a child group does not have a valid parent_id' do + group_tree_restorer.restore - expect(group_tree_restorer.restore).to be_falsey + expect(group.import_failures.first.exception_message).to eq('Parent group not found') end + end + + context 'when child group creation fails' do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:shared) { Gitlab::ImportExport::Shared.new(group) } + let(:group_tree_restorer) { described_class.new(user: user, shared: shared, group: group) } + + before do + setup_import_export_config('group_exports/child_short_name') + end + + it 'captures import failure' do + exception_message = 'Validation failed: Group URL is too short (minimum is 2 characters)' + + group_tree_restorer.restore - it 'fails when a child group does not have a valid parent_id' do - expect(shared.errors).to include('Parent group not found') + expect(group.import_failures.first.exception_message).to eq(exception_message) end end diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 0db038785d3..75db3167ebc 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -48,7 +48,6 @@ RSpec.describe Gitlab::ImportExport::Importer do [ Gitlab::ImportExport::AvatarRestorer, Gitlab::ImportExport::RepoRestorer, - Gitlab::ImportExport::WikiRestorer, Gitlab::ImportExport::UploadsRestorer, Gitlab::ImportExport::LfsRestorer, Gitlab::ImportExport::StatisticsRestorer, @@ -65,6 +64,20 @@ RSpec.describe Gitlab::ImportExport::Importer do end end + it 'calls RepoRestorer with project and wiki' do + wiki_repo_path = File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) + repo_path = File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) + restorer = double(Gitlab::ImportExport::RepoRestorer) + + expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: repo_path, shared: shared, project: project).and_return(restorer) + expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).with(path_to_bundle: wiki_repo_path, shared: shared, project: ProjectWiki.new(project)).and_return(restorer) + expect(Gitlab::ImportExport::RepoRestorer).to receive(:new).and_call_original + + expect(restorer).to receive(:restore).and_return(true).twice + + importer.execute + end + context 'with sample_data_template' do it 'initializes the Sample::TreeRestorer' do project.create_or_update_import_data(data: { sample_data: true }) diff --git a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb index 0af74dee604..2a5e802bdc5 100644 --- a/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb +++ b/spec/lib/gitlab/import_export/json/ndjson_writer_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Gitlab::ImportExport::JSON::NdjsonWriter do describe "#write_relation" do context "when single relation is serialized" do - it "appends json in correct file " do + it "appends json in correct file" do relation = "relation" value = { "key" => "value_1", "key_1" => "value_1" } subject.write_relation(exportable_path, relation, value) 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 fd3b71deb37..e2bf87bf29f 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -674,10 +674,12 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do end it 'does not allow setting params that are excluded from import_export settings' do - project.create_import_data(data: { override_params: { lfs_enabled: true } }) + original_value = project.lfs_enabled? + + project.create_import_data(data: { override_params: { lfs_enabled: !original_value } }) expect(restored_project_json).to eq(true) - expect(project.lfs_enabled).to be_falsey + expect(project.lfs_enabled).to eq(original_value) end it 'overrides project feature access levels' do diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb index bd9ac6d6697..d3c14b1f8fe 100644 --- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb @@ -113,12 +113,31 @@ RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do include_examples 'logging of relations creation' end + end + + context 'using ndjson reader' do + let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' } + let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) } + + it_behaves_like 'import project successfully' + end - context 'using ndjson reader' do - let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' } - let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) } + context 'with invalid relations' do + let(:path) { 'spec/fixtures/lib/gitlab/import_export/project_with_invalid_relations/tree' } + let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) } - it_behaves_like 'import project successfully' + it 'logs the invalid relation and its errors' do + expect(relation_tree_restorer.shared.logger) + .to receive(:warn) + .with( + error_messages: "Title can't be blank. Title is invalid", + message: '[Project/Group Import] Invalid object relation built', + relation_class: 'ProjectLabel', + relation_index: 0, + relation_key: 'labels' + ).once + + relation_tree_restorer.restore end end end diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index b32ae60fbcc..a6b917457c2 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -5,35 +5,42 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::RepoRestorer do include GitHelpers + let_it_be(:project_with_repo) do + create(:project, :repository, :wiki_repo, name: 'test-repo-restorer', path: 'test-repo-restorer').tap do |p| + p.wiki.create_page('page', 'foobar', :markdown, 'created page') + end + end + + let!(:project) { create(:project) } + + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:shared) { project.import_export_shared } + + before do + allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + bundler.save + end + + after do + FileUtils.rm_rf(export_path) + end + describe 'bundle a project Git repo' do - let(:user) { create(:user) } - let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } - let!(:project) { create(:project) } - let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:shared) { project.import_export_shared } let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: project) } - before do - allow_next_instance_of(Gitlab::ImportExport) do |instance| - allow(instance).to receive(:storage_path).and_return(export_path) - end - - bundler.save - end - after do - FileUtils.rm_rf(export_path) - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - FileUtils.rm_rf(project_with_repo.repository.path_to_repo) - FileUtils.rm_rf(project.repository.path_to_repo) - end + Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path) end it 'restores the repo successfully' do + expect(project.repository.exists?).to be false expect(subject.restore).to be_truthy + + expect(project.repository.empty?).to be false end context 'when the repository already exists' do @@ -53,4 +60,35 @@ RSpec.describe Gitlab::ImportExport::RepoRestorer do end end end + + describe 'restore a wiki Git repo' do + let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_repo, shared: shared) } + let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename) } + + subject { described_class.new(path_to_bundle: bundle_path, shared: shared, project: ProjectWiki.new(project)) } + + after do + Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path) + end + + it 'restores the wiki repo successfully' do + expect(project.wiki_repository_exists?).to be false + + subject.restore + project.wiki.repository.expire_status_cache + + expect(project.wiki_repository_exists?).to be true + end + + describe 'no wiki in the bundle' do + let!(:project_without_wiki) { create(:project) } + + let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) } + + it 'does not creates an empty wiki' do + expect(subject.restore).to be true + expect(project.wiki_repository_exists?).to be false + end + end + end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index b33462b4096..a93ee051ccf 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -26,7 +26,7 @@ Issue: - weight - time_estimate - relative_position -- service_desk_reply_to +- external_author - last_edited_at - last_edited_by_id - discussion_locked @@ -219,6 +219,7 @@ MergeRequestDiff: - start_commit_sha - commits_count - files_count +- sorted MergeRequestDiffCommit: - merge_request_diff_id - relative_order @@ -577,6 +578,8 @@ ProjectFeature: - pages_access_level - metrics_dashboard_access_level - requirements_access_level +- analytics_access_level +- operations_access_level - created_at - updated_at ProtectedBranch::MergeAccessLevel: @@ -742,6 +745,7 @@ Board: - updated_at - group_id - milestone_id +- iteration_id - weight - name - hide_backlog_list diff --git a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb b/spec/lib/gitlab/import_export/wiki_restorer_spec.rb deleted file mode 100644 index 6c80c410d07..00000000000 --- a/spec/lib/gitlab/import_export/wiki_restorer_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::ImportExport::WikiRestorer do - describe 'restore a wiki Git repo' do - let!(:project_with_wiki) { create(:project, :wiki_repo) } - let!(:project_without_wiki) { create(:project) } - let!(:project) { create(:project) } - let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:shared) { project.import_export_shared } - let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) } - let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } - let(:restorer) do - described_class.new(path_to_bundle: bundle_path, - shared: shared, - project: project.wiki, - wiki_enabled: true) - end - - before do - allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) - - bundler.save - end - - after do - FileUtils.rm_rf(export_path) - Gitlab::Shell.new.remove_repository(project_with_wiki.wiki.repository_storage, project_with_wiki.wiki.disk_path) - Gitlab::Shell.new.remove_repository(project.wiki.repository_storage, project.wiki.disk_path) - end - - it 'restores the wiki repo successfully' do - expect(restorer.restore).to be true - end - - describe "no wiki in the bundle" do - let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_without_wiki, shared: shared) } - - it 'creates an empty wiki' do - expect(restorer.restore).to be true - - expect(project.wiki_repository_exists?).to be true - end - end - end -end diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index 0dfd8a2ee50..416d651b0de 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -53,7 +53,6 @@ RSpec.describe Gitlab::ImportSources do bitbucket bitbucket_server gitlab - google_code fogbugz gitlab_project gitea @@ -70,7 +69,7 @@ RSpec.describe Gitlab::ImportSources do 'bitbucket' => Gitlab::BitbucketImport::Importer, 'bitbucket_server' => Gitlab::BitbucketServerImport::Importer, 'gitlab' => Gitlab::GitlabImport::Importer, - 'google_code' => Gitlab::GoogleCodeImport::Importer, + 'google_code' => nil, 'fogbugz' => Gitlab::FogbugzImport::Importer, 'git' => nil, 'gitlab_project' => Gitlab::ImportExport::Importer, diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index 88f2def34d9..c00b0fdf043 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -34,7 +34,10 @@ RSpec.describe Gitlab::InstrumentationHelper do :redis_shared_state_calls, :redis_shared_state_duration_s, :redis_shared_state_read_bytes, - :redis_shared_state_write_bytes + :redis_shared_state_write_bytes, + :db_count, + :db_write_count, + :db_cached_count ] expect(described_class.keys).to eq(expected_keys) @@ -46,10 +49,10 @@ RSpec.describe Gitlab::InstrumentationHelper do subject { described_class.add_instrumentation_data(payload) } - it 'adds nothing' do + it 'adds only DB counts by default' do subject - expect(payload).to eq({}) + expect(payload).to eq(db_count: 0, db_cached_count: 0, db_write_count: 0) end context 'when Gitaly calls are made' do diff --git a/spec/lib/gitlab/kubernetes/deployment_spec.rb b/spec/lib/gitlab/kubernetes/deployment_spec.rb new file mode 100644 index 00000000000..2433e854e5b --- /dev/null +++ b/spec/lib/gitlab/kubernetes/deployment_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Kubernetes::Deployment do + include KubernetesHelpers + + let(:pods) { {} } + + subject(:deployment) { described_class.new(params, pods: pods) } + + describe '#name' do + let(:params) { named(:selected) } + + it { expect(deployment.name).to eq(:selected) } + end + + describe '#labels' do + let(:params) { make('metadata', 'labels' => :selected) } + + it { expect(deployment.labels).to eq(:selected) } + end + + describe '#outdated?' do + context 'when outdated' do + let(:params) { generation(2, 1, 0) } + + it { expect(deployment.outdated?).to be_truthy } + end + + context 'when up to date' do + let(:params) { generation(2, 2, 0) } + + it { expect(deployment.outdated?).to be_falsy } + end + + context 'when ahead of latest' do + let(:params) { generation(1, 2, 0) } + + it { expect(deployment.outdated?).to be_falsy } + end + end + + describe '#instances' do + context 'when unnamed' do + let(:pods) do + [ + kube_pod(name: nil, status: 'Pending'), + kube_pod(name: nil, status: 'Pending'), + kube_pod(name: nil, status: 'Pending'), + kube_pod(name: nil, status: 'Pending') + ] + end + + let(:params) { combine(generation(1, 1, 4)) } + + it 'returns all pods with generated names and pending' do + expected = [ + { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Pending)', track: 'stable', stable: true } + ] + + expect(deployment.instances).to eq(expected) + end + end + + # When replica count is higher than pods it is considered that pod was not + # able to spawn for some reason like limited resources. + context 'when number of pods is less than wanted replicas' do + let(:wanted_replicas) { 3 } + let(:pods) { [kube_pod(name: nil, status: 'Running')] } + let(:params) { combine(generation(1, 1, wanted_replicas)) } + + it 'returns not spawned pods as pending and unknown and running' do + expected = [ + { status: 'running', pod_name: 'generated-name-with-suffix', tooltip: 'generated-name-with-suffix (Running)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'Not provided', tooltip: 'Not provided (Pending)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'Not provided', tooltip: 'Not provided (Pending)', track: 'stable', stable: true } + ] + + expect(deployment.instances).to eq(expected) + end + end + + context 'when outdated' do + let(:pods) do + [ + kube_pod(status: 'Pending'), + kube_pod(name: 'kube-pod1', status: 'Pending'), + kube_pod(name: 'kube-pod2', status: 'Pending'), + kube_pod(name: 'kube-pod3', status: 'Pending') + ] + end + + let(:params) { combine(named('foo'), generation(1, 0, 4)) } + + it 'returns all instances as named and waiting' do + expected = [ + { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'kube-pod1', tooltip: 'kube-pod1 (Pending)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'kube-pod2', tooltip: 'kube-pod2 (Pending)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'kube-pod3', tooltip: 'kube-pod3 (Pending)', track: 'stable', stable: true } + ] + + expect(deployment.instances).to eq(expected) + end + end + + context 'with pods of each type' do + let(:pods) do + [ + kube_pod(status: 'Succeeded'), + kube_pod(name: 'kube-pod1', status: 'Running'), + kube_pod(name: 'kube-pod2', status: 'Pending'), + kube_pod(name: 'kube-pod3', status: 'Pending') + ] + end + + let(:params) { combine(named('foo'), generation(1, 1, 4)) } + + it 'returns all instances' do + expected = [ + { status: 'succeeded', pod_name: 'kube-pod', tooltip: 'kube-pod (Succeeded)', track: 'stable', stable: true }, + { status: 'running', pod_name: 'kube-pod1', tooltip: 'kube-pod1 (Running)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'kube-pod2', tooltip: 'kube-pod2 (Pending)', track: 'stable', stable: true }, + { status: 'pending', pod_name: 'kube-pod3', tooltip: 'kube-pod3 (Pending)', track: 'stable', stable: true } + ] + + expect(deployment.instances).to eq(expected) + end + end + + context 'with track label' do + let(:pods) { [kube_pod(status: 'Pending')] } + let(:labels) { { 'track' => track } } + let(:params) { combine(named('foo', labels), generation(1, 0, 1)) } + + context 'when marked as stable' do + let(:track) { 'stable' } + + it 'returns all instances' do + expected = [ + { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'stable', stable: true } + ] + + expect(deployment.instances).to eq(expected) + end + end + + context 'when marked as canary' do + let(:track) { 'canary' } + let(:pods) { [kube_pod(status: 'Pending', track: track)] } + + it 'returns all instances' do + expected = [ + { status: 'pending', pod_name: 'kube-pod', tooltip: 'kube-pod (Pending)', track: 'canary', stable: false } + ] + + expect(deployment.instances).to eq(expected) + end + end + end + end + + def generation(expected, observed, replicas) + combine( + make('metadata', 'generation' => expected), + make('status', 'observedGeneration' => observed), + make('spec', 'replicas' => replicas) + ) + end + + def named(name = "foo", labels = {}) + make('metadata', 'name' => name, 'labels' => labels) + end + + def make(key, values = {}) + hsh = {} + hsh[key] = values + hsh + end + + def combine(*hashes) + out = {} + hashes.each { |hsh| out = out.deep_merge(hsh) } + out + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb index 9e580cea397..2a3a4cec2b0 100644 --- a/spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/v2/reset_command_spec.rb @@ -12,32 +12,14 @@ RSpec.describe Gitlab::Kubernetes::Helm::V2::ResetCommand do it_behaves_like 'helm command generator' do let(:commands) do <<~EOS - helm reset - kubectl delete replicaset -n gitlab-managed-apps -l name\\=tiller - kubectl delete clusterrolebinding tiller-admin + export HELM_HOST="localhost:44134" + tiller -listen ${HELM_HOST} -alsologtostderr & + helm init --client-only + helm reset --force EOS end end - context 'when there is a ca.pem file' do - let(:files) { { 'ca.pem': 'some file content' } } - - it_behaves_like 'helm command generator' do - let(:commands) do - <<~EOS1.squish + "\n" + <<~EOS2 - helm reset - --tls - --tls-ca-cert /data/helm/helm/config/ca.pem - --tls-cert /data/helm/helm/config/cert.pem - --tls-key /data/helm/helm/config/key.pem - EOS1 - kubectl delete replicaset -n gitlab-managed-apps -l name\\=tiller - kubectl delete clusterrolebinding tiller-admin - EOS2 - end - end - end - describe '#pod_name' do subject { reset_command.pod_name } diff --git a/spec/lib/gitlab/kubernetes/ingress_spec.rb b/spec/lib/gitlab/kubernetes/ingress_spec.rb new file mode 100644 index 00000000000..e4d6bf4086f --- /dev/null +++ b/spec/lib/gitlab/kubernetes/ingress_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Kubernetes::Ingress do + include KubernetesHelpers + + let(:ingress) { described_class.new(params) } + + describe '#canary?' do + subject { ingress.canary? } + + context 'with canary ingress parameters' do + let(:params) { canary_metadata } + + it { is_expected.to be_truthy } + end + + context 'with stable ingress parameters' do + let(:params) { stable_metadata } + + it { is_expected.to be_falsey } + end + end + + describe '#canary_weight' do + subject { ingress.canary_weight } + + context 'with canary ingress parameters' do + let(:params) { canary_metadata } + + it { is_expected.to eq(50) } + end + + context 'with stable ingress parameters' do + let(:params) { stable_metadata } + + it { is_expected.to be_nil } + end + end + + describe '#name' do + subject { ingress.name } + + let(:params) { stable_metadata } + + it { is_expected.to eq('production-auto-deploy') } + end + + def stable_metadata + kube_ingress(track: :stable) + end + + def canary_metadata + kube_ingress(track: :canary) + end +end diff --git a/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb b/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb new file mode 100644 index 00000000000..3ac97ddc75d --- /dev/null +++ b/spec/lib/gitlab/kubernetes/rollout_instances_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Kubernetes::RolloutInstances do + include KubernetesHelpers + + def setup(deployments_attrs, pods_attrs) + deployments = deployments_attrs.map do |attrs| + ::Gitlab::Kubernetes::Deployment.new(attrs, pods: pods_attrs) + end + + pods = pods_attrs.map do |attrs| + ::Gitlab::Kubernetes::Pod.new(attrs) + end + + [deployments, pods] + end + + describe '#pod_instances' do + it 'returns an instance for a deployment with one pod' do + deployments, pods = setup( + [kube_deployment(name: 'one', track: 'stable', replicas: 1)], + [kube_pod(name: 'one', status: 'Running', track: 'stable')] + ) + rollout_instances = described_class.new(deployments, pods) + + expect(rollout_instances.pod_instances).to eq([{ + pod_name: 'one', + stable: true, + status: 'running', + tooltip: 'one (Running)', + track: 'stable' + }]) + end + + it 'returns a pending pod for a missing replica' do + deployments, pods = setup( + [kube_deployment(name: 'one', track: 'stable', replicas: 1)], + [] + ) + rollout_instances = described_class.new(deployments, pods) + + expect(rollout_instances.pod_instances).to eq([{ + pod_name: 'Not provided', + stable: true, + status: 'pending', + tooltip: 'Not provided (Pending)', + track: 'stable' + }]) + end + + it 'returns instances when there are two stable deployments' do + deployments, pods = setup([ + kube_deployment(name: 'one', track: 'stable', replicas: 1), + kube_deployment(name: 'two', track: 'stable', replicas: 1) + ], [ + kube_pod(name: 'one', status: 'Running', track: 'stable'), + kube_pod(name: 'two', status: 'Running', track: 'stable') + ]) + rollout_instances = described_class.new(deployments, pods) + + expect(rollout_instances.pod_instances).to eq([{ + pod_name: 'one', + stable: true, + status: 'running', + tooltip: 'one (Running)', + track: 'stable' + }, { + pod_name: 'two', + stable: true, + status: 'running', + tooltip: 'two (Running)', + track: 'stable' + }]) + end + + it 'returns instances for two deployments with different tracks' do + deployments, pods = setup([ + kube_deployment(name: 'one', track: 'mytrack', replicas: 1), + kube_deployment(name: 'two', track: 'othertrack', replicas: 1) + ], [ + kube_pod(name: 'one', status: 'Running', track: 'mytrack'), + kube_pod(name: 'two', status: 'Running', track: 'othertrack') + ]) + rollout_instances = described_class.new(deployments, pods) + + expect(rollout_instances.pod_instances).to eq([{ + pod_name: 'one', + stable: false, + status: 'running', + tooltip: 'one (Running)', + track: 'mytrack' + }, { + pod_name: 'two', + stable: false, + status: 'running', + tooltip: 'two (Running)', + track: 'othertrack' + }]) + end + + it 'sorts stable tracks after canary tracks' do + deployments, pods = setup([ + kube_deployment(name: 'one', track: 'stable', replicas: 1), + kube_deployment(name: 'two', track: 'canary', replicas: 1) + ], [ + kube_pod(name: 'one', status: 'Running', track: 'stable'), + kube_pod(name: 'two', status: 'Running', track: 'canary') + ]) + rollout_instances = described_class.new(deployments, pods) + + expect(rollout_instances.pod_instances).to eq([{ + pod_name: 'two', + stable: false, + status: 'running', + tooltip: 'two (Running)', + track: 'canary' + }, { + pod_name: 'one', + stable: true, + status: 'running', + tooltip: 'one (Running)', + track: 'stable' + }]) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/rollout_status_spec.rb b/spec/lib/gitlab/kubernetes/rollout_status_spec.rb new file mode 100644 index 00000000000..8ed9fdd799c --- /dev/null +++ b/spec/lib/gitlab/kubernetes/rollout_status_spec.rb @@ -0,0 +1,271 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Kubernetes::RolloutStatus do + include KubernetesHelpers + + let(:track) { nil } + let(:specs) { specs_all_finished } + + let(:pods) do + create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: "canary") + end + + let(:ingresses) { [] } + + let(:specs_all_finished) do + [ + kube_deployment(name: 'one'), + kube_deployment(name: 'two', track: track) + ] + end + + let(:specs_half_finished) do + [ + kube_deployment(name: 'one'), + kube_deployment(name: 'two', track: track) + ] + end + + subject(:rollout_status) { described_class.from_deployments(*specs, pods_attrs: pods, ingresses: ingresses) } + + describe '#deployments' do + it 'stores the deployments' do + expect(rollout_status.deployments).to be_kind_of(Array) + expect(rollout_status.deployments.size).to eq(2) + expect(rollout_status.deployments.first).to be_kind_of(::Gitlab::Kubernetes::Deployment) + end + end + + describe '#instances' do + context 'for stable track' do + let(:track) { "any" } + + let(:pods) do + create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: "any") + end + + it 'stores the union of deployment instances' do + expected = [ + { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false }, + { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false }, + { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'any', stable: false }, + { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true }, + { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true }, + { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true } + ] + + expect(rollout_status.instances).to eq(expected) + end + end + + context 'for stable track' do + let(:track) { 'canary' } + + let(:pods) do + create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track) + end + + it 'sorts stable instances last' do + expected = [ + { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false }, + { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false }, + { status: 'running', pod_name: "two", tooltip: 'two (Running)', track: 'canary', stable: false }, + { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true }, + { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true }, + { status: 'running', pod_name: "one", tooltip: 'one (Running)', track: 'stable', stable: true } + ] + + expect(rollout_status.instances).to eq(expected) + end + end + end + + describe '#completion' do + subject { rollout_status.completion } + + context 'when all instances are finished' do + let(:track) { 'canary' } + + it { is_expected.to eq(100) } + end + + context 'when half of the instances are finished' do + let(:track) { "canary" } + + let(:pods) do + create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track, status: "Pending") + end + + let(:specs) { specs_half_finished } + + it { is_expected.to eq(50) } + end + + context 'with one deployment' do + it 'sets the completion percentage when a deployment has more running pods than desired' do + deployments = [kube_deployment(name: 'one', track: 'one', replicas: 2)] + pods = create_pods(name: 'one', track: 'one', count: 3) + rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods) + + expect(rollout_status.completion).to eq(100) + end + end + + context 'with two deployments on different tracks' do + it 'sets the completion percentage when all pods are complete' do + deployments = [ + kube_deployment(name: 'one', track: 'one', replicas: 2), + kube_deployment(name: 'two', track: 'two', replicas: 2) + ] + pods = create_pods(name: 'one', track: 'one', count: 2) + create_pods(name: 'two', track: 'two', count: 2) + rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods) + + expect(rollout_status.completion).to eq(100) + end + end + + context 'with two deployments that both have track set to "stable"' do + it 'sets the completion percentage when all pods are complete' do + deployments = [ + kube_deployment(name: 'one', track: 'stable', replicas: 2), + kube_deployment(name: 'two', track: 'stable', replicas: 2) + ] + pods = create_pods(name: 'one', track: 'stable', count: 2) + create_pods(name: 'two', track: 'stable', count: 2) + rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods) + + expect(rollout_status.completion).to eq(100) + end + + it 'sets the completion percentage when no pods are complete' do + deployments = [ + kube_deployment(name: 'one', track: 'stable', replicas: 3), + kube_deployment(name: 'two', track: 'stable', replicas: 7) + ] + rollout_status = described_class.from_deployments(*deployments, pods_attrs: []) + + expect(rollout_status.completion).to eq(0) + end + + it 'sets the completion percentage when a quarter of the pods are complete' do + deployments = [ + kube_deployment(name: 'one', track: 'stable', replicas: 6), + kube_deployment(name: 'two', track: 'stable', replicas: 2) + ] + pods = create_pods(name: 'one', track: 'stable', count: 2) + rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods) + + expect(rollout_status.completion).to eq(25) + end + end + + context 'with two deployments, one with track set to "stable" and one with no track label' do + it 'sets the completion percentage when all pods are complete' do + deployments = [ + kube_deployment(name: 'one', track: 'stable', replicas: 3), + kube_deployment(name: 'two', track: nil, replicas: 3) + ] + pods = create_pods(name: 'one', track: 'stable', count: 3) + create_pods(name: 'two', track: nil, count: 3) + rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods) + + expect(rollout_status.completion).to eq(100) + end + + it 'sets the completion percentage when no pods are complete' do + deployments = [ + kube_deployment(name: 'one', track: 'stable', replicas: 1), + kube_deployment(name: 'two', track: nil, replicas: 1) + ] + rollout_status = described_class.from_deployments(*deployments, pods_attrs: []) + + expect(rollout_status.completion).to eq(0) + end + + it 'sets the completion percentage when a third of the pods are complete' do + deployments = [ + kube_deployment(name: 'one', track: 'stable', replicas: 2), + kube_deployment(name: 'two', track: nil, replicas: 7) + ] + pods = create_pods(name: 'one', track: 'stable', count: 2) + create_pods(name: 'two', track: nil, count: 1) + rollout_status = described_class.from_deployments(*deployments, pods_attrs: pods) + + expect(rollout_status.completion).to eq(33) + end + end + end + + describe '#complete?' do + subject { rollout_status.complete? } + + context 'when all instances are finished' do + let(:track) { 'canary' } + + it { is_expected.to be_truthy } + end + + context 'when half of the instances are finished' do + let(:track) { "canary" } + + let(:pods) do + create_pods(name: "one", count: 3, track: 'stable') + create_pods(name: "two", count: 3, track: track, status: "Pending") + end + + let(:specs) { specs_half_finished } + + it { is_expected.to be_falsy} + end + end + + describe '#found?' do + context 'when the specs are passed' do + it { is_expected.to be_found } + end + + context 'when list of specs is empty' do + let(:specs) { [] } + + it { is_expected.not_to be_found } + end + end + + describe '.loading' do + subject { described_class.loading } + + it { is_expected.to be_loading } + end + + describe '#not_found?' do + context 'when the specs are passed' do + it { is_expected.not_to be_not_found } + end + + context 'when list of specs is empty' do + let(:specs) { [] } + + it { is_expected.to be_not_found } + end + end + + describe '#canary_ingress_exists?' do + context 'when canary ingress exists' do + let(:ingresses) { [kube_ingress(track: :canary)] } + + it 'returns true' do + expect(rollout_status.canary_ingress_exists?).to eq(true) + end + end + + context 'when canary ingress does not exist' do + let(:ingresses) { [kube_ingress(track: :stable)] } + + it 'returns false' do + expect(rollout_status.canary_ingress_exists?).to eq(false) + end + end + end + + def create_pods(name:, count:, track: nil, status: 'Running' ) + Array.new(count, kube_pod(name: name, status: status, track: track)) + end +end diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb deleted file mode 100644 index b2a53fe1626..00000000000 --- a/spec/lib/gitlab/metrics/background_transaction_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::BackgroundTransaction do - let(:test_worker_class) { double(:class, name: 'TestWorker') } - let(:prometheus_metric) { instance_double(Prometheus::Client::Metric, base_labels: {}) } - - before do - allow(described_class).to receive(:prometheus_metric).and_return(prometheus_metric) - end - - subject { described_class.new(test_worker_class) } - - RSpec.shared_examples 'metric with worker labels' do |metric_method| - it 'measures with correct labels and value' do - value = 1 - expect(prometheus_metric).to receive(metric_method).with({ controller: 'TestWorker', action: 'perform', feature_category: '' }, value) - - subject.send(metric_method, :bau, value) - end - end - - describe '#label' do - it 'returns labels based on class name' do - expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform', feature_category: '') - end - - it 'contains only the labels defined for metrics' do - expect(subject.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABEL_KEYS) - end - - it 'includes the feature category if there is one' do - expect(test_worker_class).to receive(:get_feature_category).and_return('source_code_management') - expect(subject.labels).to include(feature_category: 'source_code_management') - end - end - - describe '#increment' do - let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, :increment, base_labels: {}) } - - it_behaves_like 'metric with worker labels', :increment - end - - describe '#set' do - let(:prometheus_metric) { instance_double(Prometheus::Client::Gauge, :set, base_labels: {}) } - - it_behaves_like 'metric with worker labels', :set - end - - describe '#observe' do - let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, :observe, base_labels: {}) } - - it_behaves_like 'metric with worker labels', :observe - end -end diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb deleted file mode 100644 index 047d1e5d205..00000000000 --- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Metrics::SidekiqMiddleware do - let(:middleware) { described_class.new } - let(:message) { { 'args' => ['test'], 'enqueued_at' => Time.new(2016, 6, 23, 6, 59).to_f } } - - describe '#call' do - it 'tracks the transaction' do - worker = double(:worker, class: double(:class, name: 'TestWorker')) - - expect_next_instance_of(Gitlab::Metrics::BackgroundTransaction) do |transaction| - expect(transaction).to receive(:set).with(:gitlab_transaction_sidekiq_queue_duration_total, instance_of(Float)) - expect(transaction).to receive(:increment).with(:gitlab_transaction_db_count_total, 1) - end - - middleware.call(worker, message, :test) do - ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') - end - end - - it 'prevents database counters from leaking to the next transaction' do - worker = double(:worker, class: double(:class, name: 'TestWorker')) - - 2.times do - Gitlab::WithRequestStore.with_request_store do - middleware.call(worker, message, :test) do - ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') - end - end - end - - expect(message).to include(db_count: 1, db_write_count: 0, db_cached_count: 0) - end - - it 'tracks the transaction (for messages without `enqueued_at`)', :aggregate_failures do - worker = double(:worker, class: double(:class, name: 'TestWorker')) - - expect(Gitlab::Metrics::BackgroundTransaction).to receive(:new) - .with(worker.class) - .and_call_original - - expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set) - .with(:gitlab_transaction_sidekiq_queue_duration_total, instance_of(Float)) - - middleware.call(worker, {}, :test) { nil } - end - - it 'tracks any raised exceptions', :aggregate_failures, :request_store do - worker = double(:worker, class: double(:class, name: 'TestWorker')) - - expect_any_instance_of(Gitlab::Metrics::Transaction) - .to receive(:add_event).with(:sidekiq_exception) - - expect do - middleware.call(worker, message, :test) do - ActiveRecord::Base.connection.execute('SELECT pg_sleep(0.1);') - raise RuntimeError - end - end.to raise_error(RuntimeError) - - expect(message).to include(db_count: 1, db_write_count: 0, db_cached_count: 0) - end - end -end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index a31686b8061..edcd5b31941 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -18,59 +18,73 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do end describe '#sql' do - describe 'without a current transaction' do - it 'simply returns' do - expect_any_instance_of(Gitlab::Metrics::Transaction) - .not_to receive(:increment) + shared_examples 'track query in metrics' do + before do + allow(subscriber).to receive(:current_transaction) + .at_least(:once) + .and_return(transaction) + end + + it 'increments only db count value' do + described_class::DB_COUNTERS.each do |counter| + prometheus_counter = "gitlab_transaction_#{counter}_total".to_sym + if expected_counters[counter] > 0 + expect(transaction).to receive(:increment).with(prometheus_counter, 1) + else + expect(transaction).not_to receive(:increment).with(prometheus_counter, 1) + end + end subscriber.sql(event) end end - describe 'with a current transaction' do - shared_examples 'track executed query' do - before do - allow(subscriber).to receive(:current_transaction) - .at_least(:once) - .and_return(transaction) - end + shared_examples 'track query in RequestStore' do + context 'when RequestStore is enabled' do + it 'caches db count value', :request_store, :aggregate_failures do + subscriber.sql(event) - it 'increments only db count value' do described_class::DB_COUNTERS.each do |counter| - prometheus_counter = "gitlab_transaction_#{counter}_total".to_sym - if expected_counters[counter] > 0 - expect(transaction).to receive(:increment).with(prometheus_counter, 1) - else - expect(transaction).not_to receive(:increment).with(prometheus_counter, 1) - end + expect(Gitlab::SafeRequestStore[counter].to_i).to eq expected_counters[counter] end - - subscriber.sql(event) end - context 'when RequestStore is enabled' do - it 'caches db count value', :request_store, :aggregate_failures do - subscriber.sql(event) + it 'prevents db counters from leaking to the next transaction' do + 2.times do + Gitlab::WithRequestStore.with_request_store do + subscriber.sql(event) - described_class::DB_COUNTERS.each do |counter| - expect(Gitlab::SafeRequestStore[counter].to_i).to eq expected_counters[counter] + described_class::DB_COUNTERS.each do |counter| + expect(Gitlab::SafeRequestStore[counter].to_i).to eq expected_counters[counter] + end end end + end + end + end + + describe 'without a current transaction' do + it 'does not track any metrics' do + expect_any_instance_of(Gitlab::Metrics::Transaction) + .not_to receive(:increment) - it 'prevents db counters from leaking to the next transaction' do - 2.times do - Gitlab::WithRequestStore.with_request_store do - subscriber.sql(event) + subscriber.sql(event) + end - described_class::DB_COUNTERS.each do |counter| - expect(Gitlab::SafeRequestStore[counter].to_i).to eq expected_counters[counter] - end - end - end - end + context 'with read query' do + let(:expected_counters) do + { + db_count: 1, + db_write_count: 0, + db_cached_count: 0 + } end + + it_behaves_like 'track query in RequestStore' end + end + describe 'with a current transaction' do it 'observes sql_duration metric' do expect(subscriber).to receive(:current_transaction) .at_least(:once) @@ -96,12 +110,14 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do } end - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' context 'with only select' do let(:payload) { { sql: 'WITH active_milestones AS (SELECT COUNT(*), state FROM milestones GROUP BY state) SELECT * FROM active_milestones' } } - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' end end @@ -117,33 +133,38 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do context 'with select for update sql event' do let(:payload) { { sql: 'SELECT * FROM users WHERE id = 10 FOR UPDATE' } } - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' end context 'with common table expression' do context 'with insert' do let(:payload) { { sql: 'WITH archived_rows AS (SELECT * FROM users WHERE archived = true) INSERT INTO products_log SELECT * FROM archived_rows' } } - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' end end context 'with delete sql event' do let(:payload) { { sql: 'DELETE FROM users where id = 10' } } - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' end context 'with insert sql event' do let(:payload) { { sql: 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' } } - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' end context 'with update sql event' do let(:payload) { { sql: 'UPDATE users SET admin = true WHERE id = 10' } } - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' end end @@ -164,18 +185,20 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do } end - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' end context 'with cached payload name' do let(:payload) do { - sql: 'SELECT * FROM users WHERE id = 10', - name: 'CACHE' + sql: 'SELECT * FROM users WHERE id = 10', + name: 'CACHE' } end - it_behaves_like 'track executed query' + it_behaves_like 'track query in metrics' + it_behaves_like 'track query in RequestStore' end end @@ -227,8 +250,8 @@ RSpec.describe Gitlab::Metrics::Subscribers::ActiveRecord do it 'skips schema/begin/commit sql commands' do allow(subscriber).to receive(:current_transaction) - .at_least(:once) - .and_return(transaction) + .at_least(:once) + .and_return(transaction) expect(transaction).not_to receive(:increment) diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 88293f11149..d4e5a1a94f2 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -20,14 +20,6 @@ RSpec.describe Gitlab::Metrics::Transaction do end end - describe '#thread_cpu_duration' do - it 'returns the duration of a transaction in seconds' do - transaction.run { } - - expect(transaction.thread_cpu_duration).to be > 0 - end - end - describe '#run' do it 'yields the supplied block' do expect { |b| transaction.run(&b) }.to yield_control diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb index 6903ce53f65..6ee9564ef75 100644 --- a/spec/lib/gitlab/metrics/web_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb @@ -80,13 +80,15 @@ RSpec.describe Gitlab::Metrics::WebTransaction do context 'when request goes to Grape endpoint' do before do route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)') - endpoint = double(:endpoint, route: route) + endpoint = double(:endpoint, route: route, + options: { for: API::Projects, path: [":id/archive"] }, + namespace: "/projects") env['api.endpoint'] = endpoint end it 'provides labels with the method and path of the route in the grape endpoint' do - expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive', feature_category: '' }) + expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive', feature_category: 'projects' }) end it 'contains only the labels defined for transactions' do diff --git a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb index 156a440833c..132a0e9ca78 100644 --- a/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb +++ b/spec/lib/gitlab/pagination/gitaly_keyset_pager_spec.rb @@ -57,17 +57,45 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do end context 'with branch_list_keyset_pagination feature on' do + let(:fake_request) { double(url: "#{incoming_api_projects_url}?#{query.to_query}") } + let(:branch1) { double 'branch', name: 'branch1' } + let(:branch2) { double 'branch', name: 'branch2' } + let(:branch3) { double 'branch', name: 'branch3' } + before do stub_feature_flags(branch_list_keyset_pagination: project) end context 'without keyset pagination option' do - it_behaves_like 'offset pagination' + context 'when first page is requested' do + let(:branches) { [branch1, branch2, branch3] } + + it 'keyset pagination is used with offset headers' do + allow(request_context).to receive(:request).and_return(fake_request) + allow(project.repository).to receive(:branch_count).and_return(branches.size) + + expect(finder).to receive(:execute).with(gitaly_pagination: true).and_return(branches) + expect(request_context).to receive(:header).with('X-Per-Page', '2') + expect(request_context).to receive(:header).with('X-Page', '1') + expect(request_context).to receive(:header).with('X-Next-Page', '2') + expect(request_context).to receive(:header).with('X-Prev-Page', '') + expect(request_context).to receive(:header).with('Link', kind_of(String)) + expect(request_context).to receive(:header).with('X-Total', '3') + expect(request_context).to receive(:header).with('X-Total-Pages', '2') + + pager.paginate(finder) + end + end + + context 'when second page is requested' do + let(:base_query) { { per_page: 2, page: 2 } } + + it_behaves_like 'offset pagination' + end end context 'with keyset pagination option' do let(:query) { base_query.merge(pagination: 'keyset') } - let(:fake_request) { double(url: "#{incoming_api_projects_url}?#{query.to_query}") } before do allow(request_context).to receive(:request).and_return(fake_request) @@ -75,8 +103,6 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do end context 'when next page could be available' do - let(:branch1) { double 'branch', name: 'branch1' } - let(:branch2) { double 'branch', name: 'branch2' } let(:branches) { [branch1, branch2] } let(:expected_next_page_link) { %Q(<#{incoming_api_projects_url}?#{query.merge(page_token: branch2.name).to_query}>; rel="next") } @@ -90,7 +116,6 @@ RSpec.describe Gitlab::Pagination::GitalyKeysetPager do end context 'when the current page is the last page' do - let(:branch1) { double 'branch', name: 'branch1' } let(:branches) { [branch1] } it 'uses keyset pagination without link headers' do diff --git a/spec/lib/gitlab/pagination/offset_header_builder_spec.rb b/spec/lib/gitlab/pagination/offset_header_builder_spec.rb new file mode 100644 index 00000000000..a415bad5135 --- /dev/null +++ b/spec/lib/gitlab/pagination/offset_header_builder_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder do + let(:request) { double(url: 'http://localhost') } + let(:request_context) { double(header: nil, params: { per_page: 5 }, request: request) } + + subject do + described_class.new( + request_context: request_context, per_page: 5, page: 2, + next_page: 3, prev_page: 1, total: 10, total_pages: 3 + ) + end + + describe '#execute' do + let(:basic_links) do + %{<http://localhost?page=1&per_page=5>; rel="prev", <http://localhost?page=3&per_page=5>; rel="next", <http://localhost?page=1&per_page=5>; rel="first"} + end + + let(:last_link) do + %{, <http://localhost?page=3&per_page=5>; rel="last"} + end + + def expect_basic_headers + expect(request_context).to receive(:header).with('X-Per-Page', '5') + expect(request_context).to receive(:header).with('X-Page', '2') + expect(request_context).to receive(:header).with('X-Next-Page', '3') + expect(request_context).to receive(:header).with('X-Prev-Page', '1') + expect(request_context).to receive(:header).with('Link', basic_links + last_link) + end + + it 'sets headers to request context' do + expect_basic_headers + expect(request_context).to receive(:header).with('X-Total', '10') + expect(request_context).to receive(:header).with('X-Total-Pages', '3') + + subject.execute + end + + context 'exclude total headers' do + it 'does not set total headers to request context' do + expect_basic_headers + expect(request_context).not_to receive(:header) + + subject.execute(exclude_total_headers: true) + end + end + + context 'pass data without counts' do + let(:last_link) { '' } + + it 'does not set total headers to request context' do + expect_basic_headers + expect(request_context).not_to receive(:header) + + subject.execute(data_without_counts: true) + end + end + end +end diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index f320b8a66e8..8e9f7e372c5 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -433,37 +433,85 @@ RSpec.describe Gitlab::PathRegex do it { is_expected.not_to match('gitlab.git') } end - shared_examples 'invalid snippet routes' do - it { is_expected.not_to match('gitlab-org/gitlab/snippets/1.git') } - it { is_expected.not_to match('snippets/1.git') } - it { is_expected.not_to match('gitlab-org/gitlab/snippets/') } - it { is_expected.not_to match('/gitlab-org/gitlab/snippets/1') } - it { is_expected.not_to match('gitlab-org/gitlab/snippets/foo') } - it { is_expected.not_to match('root/snippets/1') } - it { is_expected.not_to match('/snippets/1') } - it { is_expected.not_to match('snippets/') } - it { is_expected.not_to match('snippets/foo') } - end + context 'repository routes' do + # Paths that match a known container + let_it_be(:container_paths) do + [ + 'gitlab-org', + 'gitlab-org/gitlab-test', + 'gitlab-org/gitlab-test/snippets/1', + 'gitlab-org/gitlab-test/snippets/foo', # ambiguous, we allow creating a sub-group called 'snippets' + 'snippets/1' + ] + end + + # Paths that never match a container + let_it_be(:invalid_paths) do + [ + 'gitlab/', + '/gitlab', + 'gitlab/foo/', + '?gitlab', + 'git lab', + '/snippets/1', + 'snippets/foo', + 'gitlab-org/gitlab/snippets/' + ] + end + + let_it_be(:git_paths) { container_paths.map { |path| path + '.git' } } + let_it_be(:snippet_paths) { container_paths.grep(%r{snippets/\d}) } + let_it_be(:wiki_git_paths) { (container_paths - snippet_paths).map { |path| path + '.wiki.git' } } + let_it_be(:invalid_git_paths) { invalid_paths.map { |path| path + '.git' } } + + def expect_route_match(paths) + paths.each { |path| is_expected.to match(path) } + end + + def expect_no_route_match(paths) + paths.each { |path| is_expected.not_to match(path) } + end + + describe '.repository_route_regex' do + subject { %r{\A#{described_class.repository_route_regex}\z} } + + it 'matches the expected paths' do + expect_route_match(container_paths) + expect_no_route_match(invalid_paths + git_paths) + end + end - describe '.full_snippets_repository_path_regex' do - subject { described_class.full_snippets_repository_path_regex } + describe '.repository_git_route_regex' do + subject { %r{\A#{described_class.repository_git_route_regex}\z} } - it { is_expected.to match('gitlab-org/gitlab/snippets/1') } - it { is_expected.to match('snippets/1') } + it 'matches the expected paths' do + expect_route_match(git_paths + wiki_git_paths) + expect_no_route_match(container_paths + invalid_paths + invalid_git_paths) + end + end - it_behaves_like 'invalid snippet routes' - end + describe '.repository_wiki_git_route_regex' do + subject { %r{\A#{described_class.repository_wiki_git_route_regex}\z} } - describe '.personal_and_project_snippets_path_regex' do - subject { %r{\A#{described_class.personal_and_project_snippets_path_regex}\z} } + it 'matches the expected paths' do + expect_route_match(wiki_git_paths) + expect_no_route_match(git_paths + invalid_git_paths) + end - it { is_expected.to match('gitlab-org/gitlab/snippets') } - it { is_expected.to match('snippets') } + it { is_expected.not_to match('snippets/1.wiki.git') } + end - it { is_expected.not_to match('gitlab-org/gitlab/snippets/1') } - it { is_expected.not_to match('snippets/1') } + describe '.full_snippets_repository_path_regex' do + subject { described_class.full_snippets_repository_path_regex } - it_behaves_like 'invalid snippet routes' + it 'matches the expected paths' do + expect_route_match(snippet_paths) + expect_no_route_match(container_paths - snippet_paths + git_paths + invalid_paths) + end + + it { is_expected.not_to match('root/snippets/1') } + it { is_expected.not_to match('gitlab-org/gitlab-test/snippets/foo') } + end end describe '.container_image_regex' do diff --git a/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb new file mode 100644 index 00000000000..bbc8b0d67e0 --- /dev/null +++ b/spec/lib/gitlab/performance_bar/redis_adapter_when_peek_enabled_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do + include ExclusiveLeaseHelpers + + let(:peek_adapter) do + Class.new do + prepend Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled + + def initialize(client) + @client = client + end + + def save(id) + # no-op + end + end + end + + describe '#save' do + let(:client) { double } + let(:uuid) { 'foo' } + + before do + allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true) + end + + it 'stores request id and enqueues stats job' do + expect_to_obtain_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid) + expect(GitlabPerformanceBarStatsWorker).to receive(:perform_in).with(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid) + expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) + + peek_adapter.new(client).save('foo') + end + + context 'when performance_bar_stats is disabled' do + before do + stub_feature_flags(performance_bar_stats: false) + end + + it 'ignores stats processing for the request' do + expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in) + expect(client).not_to receive(:sadd) + + peek_adapter.new(client).save('foo') + end + end + + context 'when exclusive lease has been already taken' do + before do + stub_exclusive_lease_taken(GitlabPerformanceBarStatsWorker::LEASE_KEY) + end + + it 'stores request id but does not enqueue any job' do + expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in) + expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid) + + peek_adapter.new(client).save('foo') + end + end + end +end diff --git a/spec/lib/gitlab/performance_bar/stats_spec.rb b/spec/lib/gitlab/performance_bar/stats_spec.rb new file mode 100644 index 00000000000..c34c6f7b31f --- /dev/null +++ b/spec/lib/gitlab/performance_bar/stats_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::PerformanceBar::Stats do + describe '#process' do + let(:request) { fixture_file('lib/gitlab/performance_bar/peek_data.json') } + let(:redis) { double(Gitlab::Redis::SharedState) } + let(:logger) { double(Gitlab::PerformanceBar::Logger) } + let(:request_id) { 'foo' } + let(:stats) { described_class.new(redis) } + + describe '#process' do + subject(:process) { stats.process(request_id) } + + before do + allow(stats).to receive(:logger).and_return(logger) + end + + it 'logs each SQL query including its duration' do + allow(redis).to receive(:get).and_return(request) + + expect(logger).to receive(:info) + .with({ duration_ms: 1.096, filename: 'lib/gitlab/pagination/offset_pagination.rb', + filenum: 53, method: 'add_pagination_headers', request_id: 'foo', type: :sql }) + expect(logger).to receive(:info) + .with({ duration_ms: 0.817, filename: 'lib/api/helpers.rb', + filenum: 112, method: 'find_project', request_id: 'foo', type: :sql }).twice + + subject + end + + it 'logs an error when the request could not be processed' do + allow(redis).to receive(:get).and_return(nil) + + expect(logger).to receive(:error).with(message: anything) + + subject + end + end + end +end diff --git a/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb b/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb new file mode 100644 index 00000000000..aa604dfab71 --- /dev/null +++ b/spec/lib/gitlab/rack_attack/user_allowlist_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::RackAttack::UserAllowlist do + using RSpec::Parameterized::TableSyntax + + subject { described_class.new(input)} + + where(:input, :elements) do + nil | [] + '' | [] + '123' | [123] + '123,456' | [123, 456] + '123,foobar, 456,' | [123, 456] + end + + with_them do + it 'has the expected elements' do + expect(subject).to contain_exactly(*elements) + end + + it 'implements empty?' do + expect(subject.empty?).to eq(elements.empty?) + end + + it 'implements include?' do + unless elements.empty? + expect(subject).to include(elements.first) + end + end + end +end diff --git a/spec/lib/gitlab/rack_attack_spec.rb b/spec/lib/gitlab/rack_attack_spec.rb new file mode 100644 index 00000000000..d72863b0103 --- /dev/null +++ b/spec/lib/gitlab/rack_attack_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::RackAttack, :aggregate_failures do + describe '.configure' do + let(:fake_rack_attack) { class_double("Rack::Attack") } + let(:fake_rack_attack_request) { class_double("Rack::Attack::Request") } + + let(:throttles) do + { + throttle_unauthenticated: Gitlab::Throttle.unauthenticated_options, + throttle_authenticated_api: Gitlab::Throttle.authenticated_api_options, + throttle_product_analytics_collector: { limit: 100, period: 60 }, + throttle_unauthenticated_protected_paths: Gitlab::Throttle.unauthenticated_options, + throttle_authenticated_protected_paths_api: Gitlab::Throttle.authenticated_api_options, + throttle_authenticated_protected_paths_web: Gitlab::Throttle.authenticated_web_options + } + end + + before do + stub_const("Rack::Attack", fake_rack_attack) + stub_const("Rack::Attack::Request", fake_rack_attack_request) + + # Expect rather than just allow, because this is actually fairly important functionality + expect(fake_rack_attack).to receive(:throttled_response_retry_after_header=).with(true) + allow(fake_rack_attack).to receive(:throttle) + allow(fake_rack_attack).to receive(:track) + allow(fake_rack_attack).to receive(:safelist) + allow(fake_rack_attack).to receive(:blocklist) + end + + it 'extends the request class' do + described_class.configure(fake_rack_attack) + + expect(fake_rack_attack_request).to include(described_class::Request) + end + + it 'configures the safelist' do + described_class.configure(fake_rack_attack) + + expect(fake_rack_attack).to have_received(:safelist).with('throttle_bypass_header') + end + + it 'configures throttles if no dry-run was configured' do + described_class.configure(fake_rack_attack) + + throttles.each do |throttle, options| + expect(fake_rack_attack).to have_received(:throttle).with(throttle.to_s, options) + end + end + + it 'configures tracks if dry-run was configured for all throttles' do + stub_env('GITLAB_THROTTLE_DRY_RUN', '*') + + described_class.configure(fake_rack_attack) + + throttles.each do |throttle, options| + expect(fake_rack_attack).to have_received(:track).with(throttle.to_s, options) + end + expect(fake_rack_attack).not_to have_received(:throttle) + end + + it 'configures tracks and throttles with a selected set of dry-runs' do + dry_run_throttles = throttles.each_key.first(2) + regular_throttles = throttles.keys[2..-1] + stub_env('GITLAB_THROTTLE_DRY_RUN', dry_run_throttles.join(',')) + + described_class.configure(fake_rack_attack) + + dry_run_throttles.each do |throttle| + expect(fake_rack_attack).to have_received(:track).with(throttle.to_s, throttles[throttle]) + end + regular_throttles.each do |throttle| + expect(fake_rack_attack).to have_received(:throttle).with(throttle.to_s, throttles[throttle]) + end + end + + context 'user allowlist' do + subject { described_class.user_allowlist } + + it 'is empty' do + described_class.configure(fake_rack_attack) + + expect(subject).to be_empty + end + + it 'reflects GITLAB_THROTTLE_USER_ALLOWLIST' do + stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', '123,456') + described_class.configure(fake_rack_attack) + + expect(subject).to contain_exactly(123, 456) + end + end + end +end diff --git a/spec/lib/gitlab/sample_data_template_spec.rb b/spec/lib/gitlab/sample_data_template_spec.rb index 7d0d415b3af..09ca41fcfc2 100644 --- a/spec/lib/gitlab/sample_data_template_spec.rb +++ b/spec/lib/gitlab/sample_data_template_spec.rb @@ -6,8 +6,7 @@ RSpec.describe Gitlab::SampleDataTemplate do describe '.all' do it 'returns all templates' do expected = %w[ - basic - serenity_valley + sample ] expect(described_class.all).to be_an(Array) @@ -19,7 +18,7 @@ RSpec.describe Gitlab::SampleDataTemplate do subject { described_class.find(query) } context 'when there is a match' do - let(:query) { :basic } + let(:query) { :sample } it { is_expected.to be_a(described_class) } end diff --git a/spec/lib/gitlab/setup_helper/workhorse_spec.rb b/spec/lib/gitlab/setup_helper/workhorse_spec.rb new file mode 100644 index 00000000000..aa9b4595799 --- /dev/null +++ b/spec/lib/gitlab/setup_helper/workhorse_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SetupHelper::Workhorse do + describe '.make' do + subject { described_class.make } + + context 'when there is a gmake' do + it 'returns gmake' do + expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) + + expect(subject).to eq 'gmake' + end + end + + context 'when there is no gmake' do + it 'returns make' do + expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 1]) + + expect(subject).to eq 'make' + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_cluster_spec.rb b/spec/lib/gitlab/sidekiq_cluster_spec.rb index 5517abe1010..3c6ea054968 100644 --- a/spec/lib/gitlab/sidekiq_cluster_spec.rb +++ b/spec/lib/gitlab/sidekiq_cluster_spec.rb @@ -123,6 +123,14 @@ RSpec.describe Gitlab::SidekiqCluster do end end + describe '.count_by_queue' do + it 'tallies the queue counts' do + queues = [%w(foo), %w(bar baz), %w(foo)] + + expect(described_class.count_by_queue(queues)).to eq(%w(foo) => 2, %w(bar baz) => 1) + end + end + describe '.concurrency' do using RSpec::Parameterized::TableSyntax diff --git a/spec/lib/gitlab/sidekiq_death_handler_spec.rb b/spec/lib/gitlab/sidekiq_death_handler_spec.rb new file mode 100644 index 00000000000..96fef88de4e --- /dev/null +++ b/spec/lib/gitlab/sidekiq_death_handler_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SidekiqDeathHandler, :clean_gitlab_redis_queues do + describe '.handler' do + context 'when the job class has worker attributes' do + let(:test_worker) do + Class.new do + include WorkerAttributes + + urgency :low + worker_has_external_dependencies! + worker_resource_boundary :cpu + feature_category :users + end + end + + before do + stub_const('TestWorker', test_worker) + end + + it 'uses the attributes from the worker' do + expect(described_class.counter) + .to receive(:increment) + .with(queue: 'test_queue', worker: 'TestWorker', + urgency: 'low', external_dependencies: 'yes', + feature_category: 'users', boundary: 'cpu') + + described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil) + end + end + + context 'when the job class does not have worker attributes' do + before do + stub_const('TestWorker', Class.new) + end + + it 'uses blank attributes' do + expect(described_class.counter) + .to receive(:increment) + .with(queue: 'test_queue', worker: 'TestWorker', + urgency: '', external_dependencies: 'no', + feature_category: '', boundary: '') + + described_class.handler({ 'class' => 'TestWorker', 'queue' => 'test_queue' }, nil) + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb index 8ef61d4eae9..0285467ecab 100644 --- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb @@ -131,31 +131,6 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi end end - describe '#droppable?' do - where(:idempotent, :prevent_deduplication) do - # [true, false].repeated_permutation(2) - [[true, true], - [true, false], - [false, true], - [false, false]] - end - - with_them do - before do - allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(idempotent) - stub_feature_flags("disable_#{queue}_deduplication" => prevent_deduplication) - end - - it 'is droppable when all conditions are met' do - if idempotent && !prevent_deduplication - expect(duplicate_job).to be_droppable - else - expect(duplicate_job).not_to be_droppable - end - end - end - end - describe '#scheduled_at' do let(:scheduled_at) { 42 } let(:job) do @@ -181,6 +156,46 @@ RSpec.describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gi end end + describe '#idempotent?' do + context 'when worker class does not exist' do + let(:job) { { 'class' => '' } } + + it 'returns false' do + expect(duplicate_job).not_to be_idempotent + end + end + + context 'when worker class does not respond to #idempotent?' do + before do + stub_const('AuthorizedProjectsWorker', Class.new) + end + + it 'returns false' do + expect(duplicate_job).not_to be_idempotent + end + end + + context 'when worker class is not idempotent' do + before do + allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(false) + end + + it 'returns false' do + expect(duplicate_job).not_to be_idempotent + end + end + + context 'when worker class is idempotent' do + before do + allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(true) + end + + it 'returns true' do + expect(duplicate_job).to be_idempotent + end + end + end + def set_idempotency_key(key, value = '1') Sidekiq.redis { |r| r.set(key, value) } end diff --git a/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb b/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb new file mode 100644 index 00000000000..63e2e930acd --- /dev/null +++ b/spec/lib/gitlab/tracking/destinations/product_analytics_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Tracking::Destinations::ProductAnalytics do + let(:emitter) { SnowplowTracker::Emitter.new('localhost', buffer_size: 1) } + let(:tracker) { SnowplowTracker::Tracker.new(emitter, SnowplowTracker::Subject.new, 'namespace', 'app_id') } + + describe '#event' do + shared_examples 'does not send an event' do + it 'does not send an event' do + expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_struct_event) + + subject.event(allowed_category, allowed_action) + end + end + + let(:allowed_category) { 'epics' } + let(:allowed_action) { 'promote' } + let(:self_monitoring_project) { create(:project) } + + before do + stub_feature_flags(product_analytics_tracking: true) + stub_application_setting(self_monitoring_project_id: self_monitoring_project.id) + stub_application_setting(usage_ping_enabled: true) + end + + context 'with allowed event' do + it 'sends an event to Product Analytics snowplow collector' do + expect(SnowplowTracker::AsyncEmitter) + .to receive(:new) + .with(ProductAnalytics::Tracker::COLLECTOR_URL, protocol: Gitlab.config.gitlab.protocol) + .and_return(emitter) + + expect(SnowplowTracker::Tracker) + .to receive(:new) + .with(emitter, an_instance_of(SnowplowTracker::Subject), Gitlab::Tracking::SNOWPLOW_NAMESPACE, self_monitoring_project.id.to_s) + .and_return(tracker) + + freeze_time do + expect(tracker) + .to receive(:track_struct_event) + .with(allowed_category, allowed_action, 'label', 'property', 1.5, nil, (Time.now.to_f * 1000).to_i) + + subject.event(allowed_category, allowed_action, label: 'label', property: 'property', value: 1.5) + end + end + end + + context 'with non-allowed event' do + it 'does not send an event' do + expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_struct_event) + + subject.event('category', 'action') + subject.event(allowed_category, 'action') + subject.event('category', allowed_action) + end + end + + context 'when self-monitoring project does not exist' do + before do + stub_application_setting(self_monitoring_project_id: nil) + end + + include_examples 'does not send an event' + end + + context 'when product_analytics_tracking FF is disabled' do + before do + stub_feature_flags(product_analytics_tracking: false) + end + + include_examples 'does not send an event' + end + + context 'when usage ping is disabled' do + before do + stub_application_setting(usage_ping_enabled: false) + end + + include_examples 'does not send an event' + end + end +end diff --git a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb index ee63eb6de04..0e8647ad78a 100644 --- a/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb +++ b/spec/lib/gitlab/tracking/destinations/snowplow_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do it 'sends event to tracker' do allow(tracker).to receive(:track_self_describing_event).and_call_original - subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', foo: 'bar') + subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }) expect(tracker).to have_received(:track_self_describing_event) do |event, context, timestamp| expect(event.to_json[:schema]).to eq('iglu:com.gitlab/foo/jsonschema/1-0-0') @@ -71,7 +71,7 @@ RSpec.describe Gitlab::Tracking::Destinations::Snowplow do it 'does not send event to tracker' do expect_any_instance_of(SnowplowTracker::Tracker).not_to receive(:track_self_describing_event) - subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', foo: 'bar') + subject.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }) end end end diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb index 805bd92fd43..57882de0974 100644 --- a/spec/lib/gitlab/tracking_spec.rb +++ b/spec/lib/gitlab/tracking_spec.rb @@ -36,6 +36,11 @@ RSpec.describe Gitlab::Tracking do end describe '.event' do + before do + allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow).to receive(:event) + allow_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics).to receive(:event) + end + it 'delegates to snowplow destination' do expect_any_instance_of(Gitlab::Tracking::Destinations::Snowplow) .to receive(:event) @@ -43,15 +48,23 @@ RSpec.describe Gitlab::Tracking do described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5) end + + it 'delegates to ProductAnalytics destination' do + expect_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics) + .to receive(:event) + .with('category', 'action', label: 'label', property: 'property', value: 1.5, context: nil) + + described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5) + end end describe '.self_describing_event' do it 'delegates to snowplow destination' do expect_any_instance_of(Gitlab::Tracking::Destinations::Snowplow) .to receive(:self_describing_event) - .with('iglu:com.gitlab/foo/jsonschema/1-0-0', { foo: 'bar' }, context: nil) + .with('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }, context: nil) - described_class.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', foo: 'bar') + described_class.self_describing_event('iglu:com.gitlab/foo/jsonschema/1-0-0', data: { foo: 'bar' }) end end end diff --git a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb index e9fb5346eae..c0deb2aa00c 100644 --- a/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/aggregated_metrics_spec.rb @@ -8,7 +8,7 @@ RSpec.describe 'aggregated metrics' do Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(event) end - failure_message do + failure_message do |event| "Event with name: `#{event}` can not be found within `#{Gitlab::UsageDataCounters::HLLRedisCounter::KNOWN_EVENTS_PATH}`" end end diff --git a/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb new file mode 100644 index 00000000000..4a31191d75f --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/base_counter_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::BaseCounter do + describe '.fetch_supported_event' do + subject { described_class.fetch_supported_event(event_name) } + + let(:event_name) { 'generic_event' } + let(:prefix) { 'generic' } + let(:known_events) { %w[event another_event] } + + before do + allow(described_class).to receive(:prefix) { prefix } + allow(described_class).to receive(:known_events) { known_events } + end + + it 'returns the matching event' do + is_expected.to eq 'event' + end + + context 'when event is unknown' do + let(:event_name) { 'generic_unknown_event' } + + it { is_expected.to be_nil } + end + + context 'when prefix does not match the event name' do + let(:prefix) { 'special' } + + it { is_expected.to be_nil } + end + end +end diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb index f2c1d8718d7..82db3d94493 100644 --- a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb @@ -74,7 +74,19 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red end end - it 'can return the count of actions per user deduplicated ' do + context 'for SSE edit actions' do + it_behaves_like 'tracks and counts action' do + def track_action(params) + described_class.track_sse_edit_action(**params) + end + + def count_unique(params) + described_class.count_sse_edit_actions(**params) + end + end + end + + it 'can return the count of actions per user deduplicated' do described_class.track_web_ide_edit_action(author: user1) described_class.track_snippet_editor_edit_action(author: user1) described_class.track_sfe_edit_action(author: user1) diff --git a/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb new file mode 100644 index 00000000000..d018100b041 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/guest_package_event_counter_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters::GuestPackageEventCounter, :clean_gitlab_redis_shared_state do + shared_examples_for 'usage counter with totals' do |counter| + it 'increments counter and returns total count' do + expect(described_class.read(counter)).to eq(0) + + 2.times { described_class.count(counter) } + + expect(described_class.read(counter)).to eq(2) + end + end + + it 'includes the right events' do + expect(described_class::KNOWN_EVENTS.size).to eq 33 + end + + described_class::KNOWN_EVENTS.each do |event| + it_behaves_like 'usage counter with totals', event + end + + describe '.fetch_supported_event' do + subject { described_class.fetch_supported_event(event_name) } + + let(:event_name) { 'package_guest_i_package_composer_guest_push' } + + it { is_expected.to eq 'i_package_composer_guest_push' } + end +end diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb index 93704a39555..70ee9871486 100644 --- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb @@ -30,6 +30,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'search', 'source_code', 'incident_management', + 'incident_management_alerts', 'testing', 'issues_edit', 'ci_secrets_management', @@ -43,7 +44,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s 'golang_packages', 'debian_packages', 'container_packages', - 'tag_packages' + 'tag_packages', + 'snippets' ) end end diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb index 803eff05efe..bf43f7552e6 100644 --- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb @@ -118,6 +118,16 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git end end + context 'for Issue cloned actions' do + it_behaves_like 'a tracked issue edit event' do + let(:action) { described_class::ISSUE_CLONED } + + def track_action(params) + described_class.track_issue_cloned_action(**params) + end + end + end + context 'for Issue relate actions' do it_behaves_like 'a tracked issue edit event' do let(:action) { described_class::ISSUE_RELATED } diff --git a/spec/lib/gitlab/usage_data_counters/search_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/search_counter_spec.rb index b55e20ba555..17188a75ccb 100644 --- a/spec/lib/gitlab/usage_data_counters/search_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/search_counter_spec.rb @@ -20,4 +20,12 @@ RSpec.describe Gitlab::UsageDataCounters::SearchCounter, :clean_gitlab_redis_sha context 'navbar_searches counter' do it_behaves_like 'usage counter with totals', :navbar_searches end + + describe '.fetch_supported_event' do + subject { described_class.fetch_supported_event(event_name) } + + let(:event_name) { 'all_searches' } + + it { is_expected.to eq 'all_searches' } + end end diff --git a/spec/lib/gitlab/usage_data_counters_spec.rb b/spec/lib/gitlab/usage_data_counters_spec.rb new file mode 100644 index 00000000000..379a2cb778d --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UsageDataCounters do + describe '.usage_data_counters' do + subject { described_class.counters } + + it { is_expected.to all(respond_to :totals) } + it { is_expected.to all(respond_to :fallback_totals) } + end + + describe '.count' do + subject { described_class.count(event_name) } + + let(:event_name) { 'static_site_editor_views' } + + it 'increases a view counter' do + expect(Gitlab::UsageDataCounters::StaticSiteEditorCounter).to receive(:count).with('views') + + subject + end + + context 'when event_name is not defined' do + let(:event_name) { 'unknown' } + + it 'raises an exception' do + expect { subject }.to raise_error(Gitlab::UsageDataCounters::UnknownEvent) + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index d305b2c5bfe..c2d96369425 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -224,7 +224,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do gitlab: 2 }, projects_imported: { - total: 20, + total: 2, gitlab_project: 2, gitlab: 2, github: 2, @@ -248,7 +248,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do gitlab: 1 }, projects_imported: { - total: 10, + total: 1, gitlab_project: 1, gitlab: 1, github: 1, @@ -456,6 +456,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(count_data[:projects]).to eq(4) expect(count_data[:projects_asana_active]).to eq(0) expect(count_data[:projects_prometheus_active]).to eq(1) + expect(count_data[:projects_jenkins_active]).to eq(1) expect(count_data[:projects_jira_active]).to eq(4) expect(count_data[:projects_jira_server_active]).to eq(2) expect(count_data[:projects_jira_cloud_active]).to eq(2) @@ -653,6 +654,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do it { is_expected.to include(:kubernetes_agent_gitops_sync) } it { is_expected.to include(:static_site_editor_views) } + it { is_expected.to include(:package_guest_i_package_composer_guest_pull) } end describe '.usage_data_counters' do @@ -840,24 +842,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end - describe '.cycle_analytics_usage_data' do - subject { described_class.cycle_analytics_usage_data } - - it 'works when queries time out in new' do - allow(Gitlab::CycleAnalytics::UsageData) - .to receive(:new).and_raise(ActiveRecord::StatementInvalid.new('')) - - expect { subject }.not_to raise_error - end - - it 'works when queries time out in to_json' do - allow_any_instance_of(Gitlab::CycleAnalytics::UsageData) - .to receive(:to_json).and_raise(ActiveRecord::StatementInvalid.new('')) - - expect { subject }.not_to raise_error - end - end - describe '.ingress_modsecurity_usage' do subject { described_class.ingress_modsecurity_usage } @@ -1054,6 +1038,14 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end end + + describe ".system_usage_data_settings" do + subject { described_class.system_usage_data_settings } + + it 'gathers settings usage data', :aggregate_failures do + expect(subject[:settings][:ldap_encrypted_secrets_enabled]).to eq(Gitlab::Auth::Ldap::Config.encrypted_secrets.active?) + end + end end describe '.merge_requests_users', :clean_gitlab_redis_shared_state do @@ -1122,6 +1114,12 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do counter.track_web_ide_edit_action(author: user3, time: time - 3.days) counter.track_snippet_editor_edit_action(author: user3) + + counter.track_sse_edit_action(author: user1) + counter.track_sse_edit_action(author: user1) + counter.track_sse_edit_action(author: user2) + counter.track_sse_edit_action(author: user3) + counter.track_sse_edit_action(author: user2, time: time - 3.days) end it 'returns the distinct count of user actions within the specified time period' do @@ -1134,7 +1132,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do action_monthly_active_users_web_ide_edit: 2, action_monthly_active_users_sfe_edit: 2, action_monthly_active_users_snippet_editor_edit: 2, - action_monthly_active_users_ide_edit: 3 + action_monthly_active_users_ide_edit: 3, + action_monthly_active_users_sse_edit: 3 } ) end @@ -1235,7 +1234,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.redis_hll_counters } let(:categories) { ::Gitlab::UsageDataCounters::HLLRedisCounter.categories } - let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management] } + let(:ineligible_total_categories) { %w[source_code testing ci_secrets_management incident_management_alerts snippets] } it 'has all known_events' do expect(subject).to have_key(:redis_hll_counters) @@ -1256,45 +1255,21 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end - describe 'aggregated_metrics' do - shared_examples 'aggregated_metrics_for_time_range' do - context 'with product_analytics_aggregated_metrics feature flag on' do - before do - stub_feature_flags(product_analytics_aggregated_metrics: true) - end + describe '.aggregated_metrics_weekly' do + subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_weekly } - it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do - expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(aggregated_metrics_data_method).and_return(global_search_gmau: 123) - expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 }) - end - end - - context 'with product_analytics_aggregated_metrics feature flag off' do - before do - stub_feature_flags(product_analytics_aggregated_metrics: false) - end - - it 'returns empty hash', :aggregate_failures do - expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(aggregated_metrics_data_method) - expect(aggregated_metrics_payload).to be {} - end - end + it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_weekly_data).and_return(global_search_gmau: 123) + expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 }) end + end - describe '.aggregated_metrics_weekly' do - subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_weekly } - - let(:aggregated_metrics_data_method) { :aggregated_metrics_weekly_data } - - it_behaves_like 'aggregated_metrics_for_time_range' - end - - describe '.aggregated_metrics_monthly' do - subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly } - - let(:aggregated_metrics_data_method) { :aggregated_metrics_monthly_data } + describe '.aggregated_metrics_monthly' do + subject(:aggregated_metrics_payload) { described_class.aggregated_metrics_monthly } - it_behaves_like 'aggregated_metrics_for_time_range' + it 'uses ::Gitlab::UsageDataCounters::HLLRedisCounter#aggregated_metrics_data', :aggregate_failures do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:aggregated_metrics_monthly_data).and_return(global_search_gmau: 123) + expect(aggregated_metrics_payload).to eq(aggregated_metrics: { global_search_gmau: 123 }) end end @@ -1323,7 +1298,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do context 'and product_analytics FF is enabled for it' do before do - stub_feature_flags(product_analytics: project) + stub_feature_flags(product_analytics_tracking: true) create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote') create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote', collector_tstamp: 2.days.ago) @@ -1339,7 +1314,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do context 'and product_analytics FF is disabled' do before do - stub_feature_flags(product_analytics: false) + stub_feature_flags(product_analytics_tracking: false) end it 'returns an empty hash' do diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index d6b1e3b2d4b..748a8336a25 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -310,4 +310,24 @@ RSpec.describe Gitlab::UserAccess do end end end + + describe '#can_push_for_ref?' do + let(:ref) { 'test_ref' } + + context 'when user cannot push_code to a project repository (eg. as a guest)' do + it 'is false' do + project.add_user(user, :guest) + + expect(access.can_push_for_ref?(ref)).to be_falsey + end + end + + context 'when user can push_code to a project repository (eg. as a developer)' do + it 'is true' do + project.add_user(user, :developer) + + expect(access.can_push_for_ref?(ref)).to be_truthy + end + end + end end diff --git a/spec/lib/gitlab/utils/usage_data_spec.rb b/spec/lib/gitlab/utils/usage_data_spec.rb index 9c0dc69ccd1..521d6584a20 100644 --- a/spec/lib/gitlab/utils/usage_data_spec.rb +++ b/spec/lib/gitlab/utils/usage_data_spec.rb @@ -37,6 +37,36 @@ RSpec.describe Gitlab::Utils::UsageData do end end + describe '#estimate_batch_distinct_count' do + let(:relation) { double(:relation) } + + it 'delegates counting to counter class instance' do + expect_next_instance_of(Gitlab::Database::PostgresHll::BatchDistinctCounter, relation, 'column') do |instance| + expect(instance).to receive(:estimate_distinct_count) + .with(batch_size: nil, start: nil, finish: nil) + .and_return(5) + end + + expect(described_class.estimate_batch_distinct_count(relation, 'column')).to eq(5) + end + + it 'returns default fallback value when counting fails due to database error' do + stub_const("Gitlab::Utils::UsageData::FALLBACK", 15) + allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(ActiveRecord::StatementInvalid.new('')) + + expect(described_class.estimate_batch_distinct_count(relation)).to eq(15) + end + + it 'logs error and returns DISTRIBUTED_HLL_FALLBACK value when counting raises any error', :aggregate_failures do + error = StandardError.new('') + stub_const("Gitlab::Utils::UsageData::DISTRIBUTED_HLL_FALLBACK", 15) + allow(Gitlab::Database::PostgresHll::BatchDistinctCounter).to receive(:new).and_raise(error) + + expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(error) + expect(described_class.estimate_batch_distinct_count(relation)).to eq(15) + end + end + describe '#sum' do let(:relation) { double(:relation) } diff --git a/spec/lib/gitlab/uuid_spec.rb b/spec/lib/gitlab/uuid_spec.rb new file mode 100644 index 00000000000..a2e28f5a24d --- /dev/null +++ b/spec/lib/gitlab/uuid_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::UUID do + let_it_be(:name) { "GitLab" } + + describe '.v5' do + subject { described_class.v5(name) } + + before do + # This is necessary to clear memoization for testing different environments + described_class.instance_variable_set(:@default_namespace_id, nil) + end + + context 'in development' do + let_it_be(:development_proper_uuid) { "5b593e54-90f5-504b-8805-5394a4d14b94" } + + before do + allow(Rails).to receive(:env).and_return(:development) + end + + it { is_expected.to eq(development_proper_uuid) } + end + + context 'in test' do + let_it_be(:test_proper_uuid) { "5b593e54-90f5-504b-8805-5394a4d14b94" } + + it { is_expected.to eq(test_proper_uuid) } + end + + context 'in staging' do + let_it_be(:staging_proper_uuid) { "dd190b37-7754-5c7c-80a0-85621a5823ad" } + + before do + allow(Rails).to receive(:env).and_return(:staging) + end + + it { is_expected.to eq(staging_proper_uuid) } + end + + context 'in production' do + let_it_be(:production_proper_uuid) { "4961388b-9d8e-5da0-a499-3ef5da58daf0" } + + before do + allow(Rails).to receive(:env).and_return(:production) + end + + it { is_expected.to eq(production_proper_uuid) } + end + end +end diff --git a/spec/lib/gitlab/webpack/manifest_spec.rb b/spec/lib/gitlab/webpack/manifest_spec.rb index 1427bdd7d4f..08b4774dd67 100644 --- a/spec/lib/gitlab/webpack/manifest_spec.rb +++ b/spec/lib/gitlab/webpack/manifest_spec.rb @@ -97,7 +97,7 @@ RSpec.describe Gitlab::Webpack::Manifest do context "with dev server disabled" do before do allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(false) - allow(File).to receive(:read).with(::Rails.root.join("manifest_output/my_manifest.json")).and_return(manifest) + stub_file_read(::Rails.root.join("manifest_output/my_manifest.json"), content: manifest) end describe ".asset_paths" do @@ -105,7 +105,7 @@ RSpec.describe Gitlab::Webpack::Manifest do it "errors if we can't find the manifest" do allow(Gitlab.config.webpack).to receive(:manifest_filename).and_return('broken.json') - allow(File).to receive(:read).with(::Rails.root.join("manifest_output/broken.json")).and_raise(Errno::ENOENT) + stub_file_read(::Rails.root.join("manifest_output/broken.json"), error: Errno::ENOENT) expect { Gitlab::Webpack::Manifest.asset_paths("entry1") }.to raise_error(Gitlab::Webpack::Manifest::ManifestLoadError) end end diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb index e332647cf8a..ed668c52a0e 100644 --- a/spec/lib/gitlab_danger_spec.rb +++ b/spec/lib/gitlab_danger_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe GitlabDanger do let(:gitlab_danger_helper) { nil } @@ -9,7 +9,7 @@ RSpec.describe GitlabDanger do describe '.local_warning_message' do it 'returns an informational message with rules that can run' do - expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, karma, database, commit_messages, product_analytics, utility_css, pajamas') + expect(described_class.local_warning_message).to eq("==> Only the following Danger rules can be run locally: #{described_class::LOCAL_RULES.join(', ')}") end end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index 7c2758bf27e..e1b8323eb8e 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -26,18 +26,17 @@ RSpec.describe Gitlab do end it 'returns the actual Git revision' do - expect(File).to receive(:read) - .with(described_class.root.join('REVISION')) - .and_return("abc123\n") + expect_file_read(described_class.root.join('REVISION'), content: "abc123\n") expect(described_class.revision).to eq('abc123') end it 'memoizes the revision' do + stub_file_read(described_class.root.join('REVISION'), content: "abc123\n") + expect(File).to receive(:read) - .once - .with(described_class.root.join('REVISION')) - .and_return("abc123\n") + .once + .with(described_class.root.join('REVISION')) 2.times { described_class.revision } end @@ -330,4 +329,24 @@ RSpec.describe Gitlab do expect(described_class.http_proxy_env?).to eq(false) end end + + describe '.maintenance_mode?' do + it 'returns true when maintenance mode is enabled' do + stub_application_setting(maintenance_mode: true) + + expect(described_class.maintenance_mode?).to eq(true) + end + + it 'returns false when maintenance mode is disabled' do + stub_application_setting(maintenance_mode: false) + + expect(described_class.maintenance_mode?).to eq(false) + end + + it 'returns false when maintenance mode feature flag is disabled' do + stub_feature_flags(maintenance_mode: false) + + expect(described_class.maintenance_mode?).to eq(false) + end + end end diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb index c35d7e8420c..3b7892334dd 100644 --- a/spec/lib/microsoft_teams/notifier_spec.rb +++ b/spec/lib/microsoft_teams/notifier_spec.rb @@ -51,11 +51,11 @@ RSpec.describe MicrosoftTeams::Notifier do describe '#body' do it 'returns Markdown-based body when HTML was passed' do - expect(subject.send(:body, options)).to eq(body.to_json) + expect(subject.send(:body, **options)).to eq(body.to_json) end it 'fails when empty Hash was passed' do - expect { subject.send(:body, {}) }.to raise_error(ArgumentError) + expect { subject.send(:body, **{}) }.to raise_error(ArgumentError) end end end diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 932d579c3cc..bd9d197afa0 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -162,6 +162,10 @@ RSpec.describe ObjectStorage::DirectUpload do it 'enables the Workhorse client' do expect(subject[:UseWorkhorseClient]).to be true end + + it 'omits the multipart upload URLs' do + expect(subject).not_to include(:MultipartUpload) + end end context 'when only server side encryption is used' do @@ -292,6 +296,7 @@ RSpec.describe ObjectStorage::DirectUpload do context 'when IAM profile is true' do let(:use_iam_profile) { true } + let(:iam_credentials_v2_url) { "http://169.254.169.254/latest/api/token" } let(:iam_credentials_url) { "http://169.254.169.254/latest/meta-data/iam/security-credentials/" } let(:iam_credentials) do { @@ -303,6 +308,9 @@ RSpec.describe ObjectStorage::DirectUpload do end before do + # If IMDSv2 is disabled, we should still fall back to IMDSv1 + stub_request(:put, iam_credentials_v2_url) + .to_return(status: 404) stub_request(:get, iam_credentials_url) .to_return(status: 200, body: "somerole", headers: {}) stub_request(:get, "#{iam_credentials_url}somerole") @@ -310,6 +318,21 @@ RSpec.describe ObjectStorage::DirectUpload do end it_behaves_like 'a valid S3 upload without multipart data' + + context 'when IMSDv2 is available' do + let(:iam_token) { 'mytoken' } + + before do + stub_request(:put, iam_credentials_v2_url) + .to_return(status: 200, body: iam_token) + stub_request(:get, iam_credentials_url).with(headers: { "X-aws-ec2-metadata-token" => iam_token }) + .to_return(status: 200, body: "somerole", headers: {}) + stub_request(:get, "#{iam_credentials_url}somerole").with(headers: { "X-aws-ec2-metadata-token" => iam_token }) + .to_return(status: 200, body: iam_credentials.to_json, headers: {}) + end + + it_behaves_like 'a valid S3 upload without multipart data' + end end end @@ -321,6 +344,30 @@ RSpec.describe ObjectStorage::DirectUpload do stub_object_storage_multipart_init(storage_url, "myUpload") end + context 'when maximum upload size is 0' do + let(:maximum_size) { 0 } + + it 'returns maximum number of parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(100) + end + + it 'part size is minimum, 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) + end + end + + context 'when maximum upload size is < 5 MB' do + let(:maximum_size) { 1024 } + + it 'returns only 1 part' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(1) + end + + it 'part size is minimum, 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) + end + end + context 'when maximum upload size is 10MB' do let(:maximum_size) { 10.megabyte } diff --git a/spec/lib/product_analytics/tracker_spec.rb b/spec/lib/product_analytics/tracker_spec.rb index 0d0660235f1..52470c9c039 100644 --- a/spec/lib/product_analytics/tracker_spec.rb +++ b/spec/lib/product_analytics/tracker_spec.rb @@ -5,53 +5,4 @@ require 'spec_helper' RSpec.describe ProductAnalytics::Tracker do it { expect(described_class::URL).to eq('http://localhost/-/sp.js') } it { expect(described_class::COLLECTOR_URL).to eq('localhost/-/collector') } - - describe '.event' do - after do - described_class.clear_memoization(:snowplow) - end - - context 'when usage ping is enabled' do - let(:tracker) { double } - let(:project_id) { 1 } - - before do - stub_application_setting(usage_ping_enabled: true, self_monitoring_project_id: project_id) - end - - it 'sends an event to Product Analytics snowplow collector' do - expect(SnowplowTracker::AsyncEmitter) - .to receive(:new) - .with(described_class::COLLECTOR_URL, { protocol: 'http' }) - .and_return('_emitter_') - - expect(SnowplowTracker::Tracker) - .to receive(:new) - .with('_emitter_', an_instance_of(SnowplowTracker::Subject), 'gl', project_id.to_s) - .and_return(tracker) - - freeze_time do - expect(tracker) - .to receive(:track_struct_event) - .with('category', 'action', '_label_', '_property_', '_value_', nil, (Time.current.to_f * 1000).to_i) - - described_class.event('category', 'action', label: '_label_', property: '_property_', - value: '_value_', context: nil) - end - end - end - - context 'when usage ping is disabled' do - before do - stub_application_setting(usage_ping_enabled: false) - end - - it 'does not send an event' do - expect(SnowplowTracker::Tracker).not_to receive(:new) - - described_class.event('category', 'action', label: '_label_', property: '_property_', - value: '_value_', context: nil) - end - end - end end diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb index 0239c974755..2232d47234f 100644 --- a/spec/lib/quality/test_level_spec.rb +++ b/spec/lib/quality/test_level_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Quality::TestLevel do context 'when level is unit' do it 'returns a pattern' do expect(subject.pattern(:unit)) - .to eq("spec/{bin,channels,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,elastic_integration,tooling}{,/**/}*_spec.rb") + .to eq("spec/{bin,channels,config,db,dependencies,elastic,elastic_integration,experiments,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,tooling}{,/**/}*_spec.rb") end end @@ -103,7 +103,7 @@ RSpec.describe Quality::TestLevel do context 'when level is unit' do it 'returns a regexp' do expect(subject.regexp(:unit)) - .to eq(%r{spec/(bin|channels|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|elastic_integration|tooling)}) + .to eq(%r{spec/(bin|channels|config|db|dependencies|elastic|elastic_integration|experiments|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|tooling)}) end end @@ -174,6 +174,10 @@ RSpec.describe Quality::TestLevel do expect(subject.level_for('spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb')).to eq(:migration) end + it 'returns the correct level for an EE file without passing a prefix' do + expect(subject.level_for('ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb')).to eq(:migration) + end + it 'returns the correct level for a geo migration test' do expect(described_class.new('ee/').level_for('ee/spec/migrations/geo/migrate_ci_job_artifacts_to_separate_registry_spec.rb')).to eq(:migration) end |