# frozen_string_literal: true require 'spec_helper' RSpec.describe API::Lint, feature_category: :pipeline_composition do describe 'POST /ci/lint' do context 'when signup settings are disabled' do before do Gitlab::CurrentSettings.signup_enabled = false end context 'when unauthenticated' do it 'returns authentication error' do post api('/ci/lint'), params: { content: 'content' } expect(response).to have_gitlab_http_status(:unauthorized) end end context 'when authenticated' do let_it_be(:api_user) { create(:user) } it 'returns authorized' do post api('/ci/lint', api_user), params: { content: 'content' } expect(response).to have_gitlab_http_status(:ok) end end context 'when authenticated as external user' do let(:project) { create(:project) } let(:api_user) { create(:user, :external) } context 'when reporter in a project' do before do project.add_reporter(api_user) end it 'returns authorization failure' do post api('/ci/lint', api_user), params: { content: 'content' } expect(response).to have_gitlab_http_status(:unauthorized) end end context 'when developer in a project' do before do project.add_developer(api_user) end it 'returns authorization success' do post api('/ci/lint', api_user), params: { content: 'content' } expect(response).to have_gitlab_http_status(:ok) end end end end context 'when signup is enabled and not limited' do before do Gitlab::CurrentSettings.signup_enabled = true stub_application_setting(domain_allowlist: [], email_restrictions_enabled: false, require_admin_approval_after_user_signup: false) end context 'when unauthenticated' do it 'returns authorized success' do post api('/ci/lint'), params: { content: 'content' } expect(response).to have_gitlab_http_status(:ok) end end context 'when authenticated' do let_it_be(:api_user) { create(:user) } it 'returns authentication success' do post api('/ci/lint', api_user), params: { content: 'content' } expect(response).to have_gitlab_http_status(:ok) end end end context 'when limited signup is enabled' do before do stub_application_setting(domain_allowlist: ['www.gitlab.com']) Gitlab::CurrentSettings.signup_enabled = true end context 'when unauthenticated' do it 'returns unauthorized' do post api('/ci/lint'), params: { content: 'content' } expect(response).to have_gitlab_http_status(:unauthorized) end end context 'when authenticated' do let_it_be(:api_user) { create(:user) } it 'returns authentication success' do post api('/ci/lint', api_user), params: { content: 'content' } expect(response).to have_gitlab_http_status(:ok) end end end context 'when authenticated' do let_it_be(:api_user) { create(:user) } context 'with valid .gitlab-ci.yml content' do let(:yaml_content) do File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) end it 'passes validation without warnings or errors' do post api('/ci/lint', api_user), params: { content: yaml_content } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Hash expect(json_response['status']).to eq('valid') expect(json_response['warnings']).to match_array([]) expect(json_response['errors']).to match_array([]) expect(json_response['includes']).to eq([]) end it 'outputs expanded yaml content' do post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('merged_yaml') end it 'outputs jobs' do post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('jobs') end end context 'with valid .gitlab-ci.yml with warnings' do let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml } it 'passes validation but returns warnings' do post api('/ci/lint', api_user), params: { content: yaml_content } expect(response).to have_gitlab_http_status(:ok) expect(json_response['status']).to eq('valid') expect(json_response['warnings']).not_to be_empty expect(json_response['errors']).to match_array([]) end end context 'with an invalid .gitlab-ci.yml' do context 'with invalid syntax' do let(:yaml_content) { 'invalid content' } it 'responds with errors about invalid syntax' do post api('/ci/lint', api_user), params: { content: yaml_content } expect(response).to have_gitlab_http_status(:ok) expect(json_response['status']).to eq('invalid') expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq(['Invalid configuration format']) expect(json_response['includes']).to eq(nil) end it 'outputs expanded yaml content' do post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('merged_yaml') end it 'outputs jobs' do post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('jobs') end end context 'with invalid configuration' do let(:yaml_content) { '{ image: "image:1.0", services: ["postgres"] }' } it 'responds with errors about invalid configuration' do post api('/ci/lint', api_user), params: { content: yaml_content } expect(response).to have_gitlab_http_status(:ok) expect(json_response['status']).to eq('invalid') expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) expect(json_response['includes']).to eq([]) end it 'outputs expanded yaml content' do post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('merged_yaml') end it 'outputs jobs' do post api('/ci/lint', api_user), params: { content: yaml_content, include_jobs: true } expect(response).to have_gitlab_http_status(:ok) expect(json_response).to have_key('jobs') end end end context 'without the content parameter' do it 'responds with validation error about missing content' do post api('/ci/lint', api_user) expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq('content is missing') end end end end describe 'GET /projects/:id/ci/lint' do subject(:ci_lint) { get api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, include_jobs: include_jobs } } let(:project) { create(:project, :repository) } let(:dry_run) { nil } let(:include_jobs) { nil } RSpec.shared_examples 'valid config with warnings' do it 'passes validation with warnings' do ci_lint expect(response).to have_gitlab_http_status(:ok) expect(json_response['valid']).to eq(true) expect(json_response['errors']).to eq([]) expect(json_response['warnings']).not_to be_empty end end RSpec.shared_examples 'valid config without warnings' do it 'passes validation' do ci_lint included_config = YAML.safe_load(included_content, permitted_classes: [Symbol]) root_config = YAML.safe_load(yaml_content, permitted_classes: [Symbol]) expected_yaml = included_config.merge(root_config).except(:include).deep_stringify_keys.to_yaml expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Hash expect(json_response['merged_yaml']).to eq(expected_yaml) expect(json_response['includes']).to contain_exactly( { 'type' => 'local', 'location' => 'another-gitlab-ci.yml', 'blob' => "http://localhost/#{project.full_path}/-/blob/#{project.commit.sha}/another-gitlab-ci.yml", 'raw' => "http://localhost/#{project.full_path}/-/raw/#{project.commit.sha}/another-gitlab-ci.yml", 'extra' => {}, 'context_project' => project.full_path, 'context_sha' => project.commit.sha } ) expect(json_response['valid']).to eq(true) expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq([]) end end RSpec.shared_examples 'invalid config' do it 'responds with errors about invalid configuration' do ci_lint expect(response).to have_gitlab_http_status(:ok) expect(json_response['merged_yaml']).to eq(yaml_content) expect(json_response['includes']).to eq([]) expect(json_response['valid']).to eq(false) expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) end end context 'when unauthenticated' do let_it_be(:api_user) { nil } it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:not_found) end end context 'when authenticated as non-member' do let_it_be(:api_user) { create(:user) } let(:yaml_content) do { include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml end context 'when project is private' do before do project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) stub_ci_pipeline_yaml_file(yaml_content) end it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:not_found) end end context 'when project is public' do before do project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end context 'when running as dry run' do let(:dry_run) { true } before do stub_ci_pipeline_yaml_file(yaml_content) end it 'returns pipeline creation error' do ci_lint expect(response).to have_gitlab_http_status(:ok) expect(json_response['merged_yaml']).to eq(nil) expect(json_response['includes']).to eq(nil) expect(json_response['valid']).to eq(false) expect(json_response['warnings']).to eq([]) expect(json_response['errors']).to eq(['Insufficient permissions to create a new pipeline']) end end context 'when running static validation' do let(:dry_run) { false } let(:included_content) do { another_test: { stage: 'test', script: 'echo 1' } }.deep_stringify_keys.to_yaml end before do project.repository.create_file( project.creator, '.gitlab-ci.yml', yaml_content, message: 'Automatically created .gitlab-ci.yml', branch_name: 'master' ) project.repository.create_file( project.creator, 'another-gitlab-ci.yml', included_content, message: 'Automatically created another-gitlab-ci.yml', branch_name: 'master' ) end it_behaves_like 'valid config without warnings' end end end context 'when authenticated as project guest' do let_it_be(:api_user) { create(:user) } before do project.add_guest(api_user) end it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:forbidden) end end context 'when authenticated as project developer' do let_it_be(:api_user) { create(:user) } before do project.add_developer(api_user) end context 'with no commit' do it 'returns error about providing content' do ci_lint expect(response).to have_gitlab_http_status(:ok) expect(json_response['errors']).to match_array(['Please provide content of .gitlab-ci.yml']) end end context 'with valid .gitlab-ci.yml content' do let(:yaml_content) do { include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml end let(:included_content) do { another_test: { stage: 'test', script: 'echo 1' } }.deep_stringify_keys.to_yaml end before do project.repository.create_file( project.creator, '.gitlab-ci.yml', yaml_content, message: 'Automatically created .gitlab-ci.yml', branch_name: 'master' ) project.repository.create_file( project.creator, 'another-gitlab-ci.yml', included_content, message: 'Automatically created another-gitlab-ci.yml', branch_name: 'master' ) end context 'when running as dry run' do let(:dry_run) { true } it_behaves_like 'valid config without warnings' end context 'when running static validation' do let(:dry_run) { false } it_behaves_like 'valid config without warnings' end context 'when running with include jobs' do let(:include_jobs) { true } it_behaves_like 'valid config without warnings' it 'returns jobs key' do ci_lint expect(json_response).to have_key('jobs') end end context 'when running without include jobs' do let(:include_jobs) { false } it_behaves_like 'valid config without warnings' it 'does not return jobs key' do ci_lint expect(json_response).not_to have_key('jobs') end end context 'With warnings' do let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml } it_behaves_like 'valid config with warnings' end end context 'with invalid .gitlab-ci.yml content' do let(:yaml_content) do { image: 'image:1.0', services: ['postgres'] }.deep_stringify_keys.to_yaml end before do stub_ci_pipeline_yaml_file(yaml_content) end context 'when running as dry run' do let(:dry_run) { true } it_behaves_like 'invalid config' end context 'when running static validation' do let(:dry_run) { false } it_behaves_like 'invalid config' end context 'when running with include jobs' do let(:include_jobs) { true } it_behaves_like 'invalid config' it 'returns jobs key' do ci_lint expect(json_response).to have_key('jobs') end end context 'when running without include jobs' do let(:include_jobs) { false } it_behaves_like 'invalid config' it 'does not return jobs key' do ci_lint expect(json_response).not_to have_key('jobs') end end end end end describe 'POST /projects/:id/ci/lint' do subject(:ci_lint) { post api("/projects/#{project.id}/ci/lint", api_user), params: { dry_run: dry_run, content: yaml_content, include_jobs: include_jobs } } let(:project) { create(:project, :repository) } let(:dry_run) { nil } let(:include_jobs) { nil } let_it_be(:api_user) { create(:user) } let_it_be(:yaml_content) do { include: { local: 'another-gitlab-ci.yml' }, test: { stage: 'test', script: 'echo 1' } }.to_yaml end let_it_be(:included_content) do { another_test: { stage: 'test', script: 'echo 1' } }.to_yaml end RSpec.shared_examples 'valid project config' do it 'passes validation' do ci_lint included_config = YAML.safe_load(included_content, permitted_classes: [Symbol]) root_config = YAML.safe_load(yaml_content, permitted_classes: [Symbol]) expected_yaml = included_config.merge(root_config).except(:include).deep_stringify_keys.to_yaml expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Hash expect(json_response['merged_yaml']).to eq(expected_yaml) expect(json_response['includes']).to contain_exactly( { 'type' => 'local', 'location' => 'another-gitlab-ci.yml', 'blob' => "http://localhost/#{project.full_path}/-/blob/#{project.commit.sha}/another-gitlab-ci.yml", 'raw' => "http://localhost/#{project.full_path}/-/raw/#{project.commit.sha}/another-gitlab-ci.yml", 'extra' => {}, 'context_project' => project.full_path, 'context_sha' => project.commit.sha } ) expect(json_response['valid']).to eq(true) expect(json_response['errors']).to eq([]) end end RSpec.shared_examples 'invalid project config' do it 'responds with errors about invalid configuration' do ci_lint expect(response).to have_gitlab_http_status(:ok) expect(json_response['merged_yaml']).to eq(yaml_content) expect(json_response['includes']).to eq([]) expect(json_response['valid']).to eq(false) expect(json_response['errors']).to eq(['jobs config should contain at least one visible job']) end end context 'with an empty repository' do let_it_be(:empty_project) { create(:project_empty_repo) } let_it_be(:yaml_content) do File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) end before do empty_project.add_developer(api_user) end it 'passes validation without errors' do post api("/projects/#{empty_project.id}/ci/lint", api_user), params: { content: yaml_content } expect(response).to have_gitlab_http_status(:ok) expect(json_response['valid']).to eq(true) expect(json_response['errors']).to eq([]) end end context 'when unauthenticated' do let_it_be(:api_user) { nil } it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:not_found) end context 'when project is public' do before do project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:forbidden) end end end context 'when authenticated as non-member' do context 'when project is private' do before do project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) end it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:not_found) end end context 'when project is public' do before do project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end context 'when running as dry run' do let(:dry_run) { true } it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:forbidden) end end context 'when running static validation' do let(:dry_run) { false } before do project.repository.create_file( project.creator, 'another-gitlab-ci.yml', included_content, message: 'Automatically created another-gitlab-ci.yml', branch_name: 'master' ) end it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:forbidden) end end end end context 'when authenticated as project guest' do before do project.add_guest(api_user) end it 'returns authentication error' do ci_lint expect(response).to have_gitlab_http_status(:forbidden) end end context 'when authenticated as project developer' do before do project.add_developer(api_user) end context 'with valid .gitlab-ci.yml content' do before do project.repository.create_file( project.creator, 'another-gitlab-ci.yml', included_content, message: 'Automatically created another-gitlab-ci.yml', branch_name: 'master' ) end context 'when running as dry run' do let(:dry_run) { true } it_behaves_like 'valid project config' end context 'when running static validation' do let(:dry_run) { false } it_behaves_like 'valid project config' end context 'when running with include jobs param' do let(:include_jobs) { true } it_behaves_like 'valid project config' it 'contains jobs key' do ci_lint expect(json_response).to have_key('jobs') end end context 'when running without include jobs param' do let(:include_jobs) { false } it_behaves_like 'valid project config' it 'does not contain jobs key' do ci_lint expect(json_response).not_to have_key('jobs') end end end context 'with invalid .gitlab-ci.yml content' do let(:yaml_content) do { image: 'image:1.0', services: ['postgres'] }.deep_stringify_keys.to_yaml end context 'when running as dry run' do let(:dry_run) { true } it_behaves_like 'invalid project config' end context 'when running static validation' do let(:dry_run) { false } it_behaves_like 'invalid project config' end context 'when running with include jobs set to false' do let(:include_jobs) { false } it_behaves_like 'invalid project config' it 'does not contain jobs key' do ci_lint expect(json_response).not_to have_key('jobs') end end context 'when running with param include jobs' do let(:include_jobs) { true } it_behaves_like 'invalid project config' it 'contains jobs key' do ci_lint expect(json_response).to have_key('jobs') end end end end end end