# frozen_string_literal: true require 'spec_helper' RSpec.describe JsonSchemaValidator, feature_category: :shared do describe '#validates_each' do let(:build_report_result) { build(:ci_build_report_result, :with_junit_success) } subject { validator.validate(build_report_result) } context 'when filename is set' do let(:validator) { described_class.new(attributes: [:data], filename: "build_report_result_data") } context 'when data is valid' do it 'returns no errors' do subject expect(build_report_result.errors).to be_empty end end context 'when data is invalid' do context 'when error message is not provided' do it 'returns default set error message i.e `must be a valid json schema`' do build_report_result.data = { invalid: 'data' } validator.validate(build_report_result) expect(build_report_result.errors.size).to eq(1) expect(build_report_result.errors.full_messages).to eq(["Data must be a valid json schema"]) end end end context 'when error message is provided' do let(:validator) { described_class.new(attributes: [:data], filename: "build_report_result_data", message: "error in build-report-json") } it 'returns the provided error message' do build_report_result.data = { invalid: 'data' } validator.validate(build_report_result) expect(build_report_result.errors.size).to eq(1) expect(build_report_result.errors.full_messages).to eq(["Data error in build-report-json"]) end end end context 'when filename is not set' do let(:validator) { described_class.new(attributes: [:data]) } it 'raises an ArgumentError' do expect { subject }.to raise_error(ArgumentError) end end context 'when filename is invalid' do let(:validator) { described_class.new(attributes: [:data], filename: "invalid$filename") } it 'raises a FilenameError' do expect { subject }.to raise_error(described_class::FilenameError) end end describe 'hash_conversion option' do context 'when hash_conversion is enabled' do let(:validator) { described_class.new(attributes: [:data], filename: "build_report_result_data", hash_conversion: true) } it 'returns no errors' do subject expect(build_report_result.errors).to be_empty end end end context 'when detail_errors is true' do let(:validator) { described_class.new(attributes: [:data], detail_errors: true, filename: "build_report_result_data") } context 'when data is valid' do it 'returns no errors' do subject expect(build_report_result.errors).to be_empty end end context 'when data is invalid' do it 'returns json schema is invalid' do build_report_result.data = { invalid: 'data' } subject expect(build_report_result.errors.size).to eq(1) expect(build_report_result.errors.full_messages).to match_array( ["Data object property at `/invalid` is a disallowed additional property"] ) end end end context 'when validating config with oneOf JSON schema' do let(:config) do { run: [ { name: 'hello_steps', step: 'gitlab.com/gitlab-org/ci-cd/runner-tools/echo-step', inputs: { echo: 'hello steps!' } } ] } end let(:job) { Gitlab::Ci::Config::Entry::Job.new(config, name: :rspec) } let(:errors) { ActiveModel::Errors.new(job) } let(:validator) do described_class.new( attributes: [:run], base_directory: 'app/validators/json_schemas', filename: 'run_steps', hash_conversion: true, detail_errors: true ) end before do job.compose! allow(job).to receive(:errors).and_return(errors) end subject { validator.validate(job) } context 'when the value is a valid array of hashes' do before do allow(job).to receive(:read_attribute_for_validation).and_return(config[:run]) end it 'returns no errors' do subject expect(job.errors).to be_empty end end context 'when a required property is missing' do before do config[:run] = [{ name: 'hello_steps' }] allow(job).to receive(:read_attribute_for_validation).and_return(config[:run]) end it 'returns an error message' do subject expect(job.errors).not_to be_empty expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run object at `/0` is missing required properties: step") end end context 'when oneOf validation fails' do before do config[:run] = [nil] allow(job).to receive(:read_attribute_for_validation).and_return(config[:run]) end it 'returns an error message' do subject expect(job.errors).not_to be_empty expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq( "run value at `/0` is not an object" ) end end context 'when there is a general validation error' do before do config[:run] = 'not an array' allow(job).to receive(:read_attribute_for_validation).and_return(config[:run]) end it 'returns an error message' do subject expect(job.errors).not_to be_empty expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run value at root is not an array") end end context 'when a non-array value violates oneOf constraint' do let(:schema) do { "type" => "object", "properties" => { "run" => { "oneOf" => [ { required: ["step"], title: "step" }, { required: ["script"], title: "script" } ] } } } end let(:validator) do described_class.new( attributes: [:run], filename: 'test_schema', detail_errors: true ) end before do config[:run] = 'C' allow(job).to receive(:read_attribute_for_validation).and_return({ run: config[:run] }) allow(JSONSchemer).to receive(:schema).and_return(JSONSchemer.schema(schema)) allow(File).to receive(:read).with(anything).and_return(schema.to_json) end it 'returns an error message for oneOf violation without data pointer' do subject expect(job.errors).not_to be_empty expect("#{job.errors.first.attribute} #{job.errors.first.type}").to eq("run should use only one of: step, script") end end end context 'when size_limit is specified' do let(:validator) { described_class.new(attributes: [:data], filename: "build_report_result_data", size_limit: 10.kilobytes) } context 'when data exceeds size limit' do it 'returns size error message' do build_report_result.data = { large_field: 'x' * 15000 } subject expect(build_report_result.errors.size).to eq(1) expect(build_report_result.errors.full_messages.first).to include("is too large") expect(build_report_result.errors.full_messages.first).to include("10 KiB") end end context 'when data is within size limit' do it 'validates schema normally' do subject expect(build_report_result.errors).to be_empty end end end end describe '#schema' do let(:validator) { described_class.new(attributes: [:data], filename: "build_report_result_data") } it 'is instance of JSONSchemer schema' do expect(validator.schema).to be_a(JSONSchemer::Schema) end end end