# encoding: utf-8 require 'stringio' require 'spec_helper' require PROTOS_PATH.join('resource.pb') require PROTOS_PATH.join('enum.pb') RSpec.describe Protobuf::Message do describe '.decode' do let(:message) { ::Test::Resource.new(:name => "Jim") } it 'creates a new message object decoded from the given bytes' do expect(::Test::Resource.decode(message.encode)).to eq message end context 'with a new enum value' do let(:older_message) do Class.new(Protobuf::Message) do enum_class = Class.new(::Protobuf::Enum) do define :YAY, 1 end optional enum_class, :enum_field, 1 repeated enum_class, :enum_list, 2 end end let(:newer_message) do Class.new(Protobuf::Message) do enum_class = Class.new(::Protobuf::Enum) do define :YAY, 1 define :HOORAY, 2 end optional enum_class, :enum_field, 1 repeated enum_class, :enum_list, 2 end end context 'with a singular field' do it 'treats the field as if it was unset when decoding' do newer = newer_message.new(:enum_field => :HOORAY).serialize expect(older_message.decode(newer).enum_field!).to be_nil end it 'rejects an unknown value when using the constructor' do expect { older_message.new(:enum_field => :HOORAY) }.to raise_error(TypeError) end it 'rejects an unknown value when the setter' do older = older_message.new expect { older.enum_field = :HOORAY }.to raise_error(TypeError) end end context 'with a repeated field' do it 'treats the field as if it was unset when decoding' do newer = newer_message.new(:enum_list => [:HOORAY]).serialize expect(older_message.decode(newer).enum_list).to eq([]) end it 'rejects an unknown value when using the constructor' do expect { older_message.new(:enum_list => [:HOORAY]) }.to raise_error(TypeError) end it 'rejects an unknown value when the setter' do older = older_message.new expect { older.enum_field = [:HOORAY] }.to raise_error(TypeError) end end end end describe '.decode_from' do let(:message) { ::Test::Resource.new(:name => "Jim") } it 'creates a new message object decoded from the given byte stream' do stream = ::StringIO.new(message.encode) expect(::Test::Resource.decode_from(stream)).to eq message end end describe 'defining a new field' do context 'when defining a field with a tag that has already been used' do it 'raises a TagCollisionError' do expect do Class.new(Protobuf::Message) do optional ::Protobuf::Field::Int32Field, :foo, 1 optional ::Protobuf::Field::Int32Field, :bar, 1 end end.to raise_error(Protobuf::TagCollisionError, /Field number 1 has already been used/) end end context 'when defining an extension field with a tag that has already been used' do it 'raises a TagCollisionError' do expect do Class.new(Protobuf::Message) do extensions 100...110 optional ::Protobuf::Field::Int32Field, :foo, 100 optional ::Protobuf::Field::Int32Field, :bar, 100, :extension => true end end.to raise_error(Protobuf::TagCollisionError, /Field number 100 has already been used/) end end context 'when defining a field with a name that has already been used' do it 'raises a DuplicateFieldNameError' do expect do Class.new(Protobuf::Message) do optional ::Protobuf::Field::Int32Field, :foo, 1 optional ::Protobuf::Field::Int32Field, :foo, 2 end end.to raise_error(Protobuf::DuplicateFieldNameError, /Field name foo has already been used/) end end context 'when defining an extension field with a name that has already been used' do it 'raises a DuplicateFieldNameError' do expect do Class.new(Protobuf::Message) do extensions 100...110 optional ::Protobuf::Field::Int32Field, :foo, 1 optional ::Protobuf::Field::Int32Field, :foo, 100, :extension => true end end.to raise_error(Protobuf::DuplicateFieldNameError, /Field name foo has already been used/) end end end describe '.encode' do let(:values) { { :name => "Jim" } } it 'creates a new message object with the given values and returns the encoded bytes' do expect(::Test::Resource.encode(values)).to eq ::Test::Resource.new(values).encode end end describe '#initialize' do it "defaults to the first value listed in the enum's type definition" do test_enum = Test::EnumTestMessage.new expect(test_enum.non_default_enum).to eq(1) end it "defaults to a a value with a name" do test_enum = Test::EnumTestMessage.new expect(test_enum.non_default_enum.name).to eq(:ONE) end it "exposes the enum getter raw value through ! method" do test_enum = Test::EnumTestMessage.new expect(test_enum.non_default_enum!).to be_nil end it "exposes the enum getter raw value through ! method (when set)" do test_enum = Test::EnumTestMessage.new test_enum.non_default_enum = 1 expect(test_enum.non_default_enum!).to eq(1) end it "does not try to set attributes which have nil values" do expect_any_instance_of(Test::EnumTestMessage).not_to receive("non_default_enum=") Test::EnumTestMessage.new(:non_default_enum => nil) end it "takes a hash as an initialization argument" do test_enum = Test::EnumTestMessage.new(:non_default_enum => 2) expect(test_enum.non_default_enum).to eq(2) end it "initializes with an object that responds to #to_hash" do hashie_object = OpenStruct.new(:to_hash => { :non_default_enum => 2 }) test_enum = Test::EnumTestMessage.new(hashie_object) expect(test_enum.non_default_enum).to eq(2) end it "initializes with an object with a block" do test_enum = Test::EnumTestMessage.new { |p| p.non_default_enum = 2 } expect(test_enum.non_default_enum).to eq(2) end context 'ignoring unknown fields' do around do |example| orig = ::Protobuf.ignore_unknown_fields? ::Protobuf.ignore_unknown_fields = true example.call ::Protobuf.ignore_unknown_fields = orig end context 'with valid fields' do let(:values) { { :name => "Jim" } } it "does not raise an error" do expect { ::Test::Resource.new(values) }.to_not raise_error end end context 'with non-existent field' do let(:values) { { :name => "Jim", :othername => "invalid" } } it "does not raise an error" do expect { ::Test::Resource.new(values) }.to_not raise_error end end end context 'not ignoring unknown fields' do around do |example| orig = ::Protobuf.ignore_unknown_fields? ::Protobuf.ignore_unknown_fields = false example.call ::Protobuf.ignore_unknown_fields = orig end context 'with valid fields' do let(:values) { { :name => "Jim" } } it "does not raise an error" do expect { ::Test::Resource.new(values) }.to_not raise_error end end context 'with non-existent field' do let(:values) { { :name => "Jim", :othername => "invalid" } } it "raises an error and mentions the erroneous field" do expect { ::Test::Resource.new(values) }.to raise_error(::Protobuf::FieldNotDefinedError, /othername/) end context 'with a nil value' do let(:values) { { :name => "Jim", :othername => nil } } it "raises an error and mentions the erroneous field" do expect { ::Test::Resource.new(values) }.to raise_error(::Protobuf::FieldNotDefinedError, /othername/) end end end end end describe '#encode' do context "encoding" do it "accepts UTF-8 strings into string fields" do message = ::Test::Resource.new(:name => "Kyle Redfearn\u0060s iPad") expect { message.encode }.to_not raise_error end it "keeps utf-8 when utf-8 is input for string fields" do name = 'my name💩' name.force_encoding(Encoding::UTF_8) message = ::Test::Resource.new(:name => name) new_message = ::Test::Resource.decode(message.encode) expect(new_message.name == name).to be true end it "trims binary when binary is input for string fields" do name = "my name\xC3" name.force_encoding(Encoding::BINARY) message = ::Test::Resource.new(:name => name) new_message = ::Test::Resource.decode(message.encode) expect(new_message.name == "my name").to be true end end describe 'memoization' do it "should memoize enum message" do test_enum = Test::EnumTestMessage.new test_enum.encode expect(test_enum.instance_variable_get(:@encode)).to eq("") test_enum.non_default_enum = 2 expect(test_enum.instance_variable_get(:@encode)).to be_nil end context "boolean fields" do let(:values) { { :ext_is_searchable => true, :name => "STEPH CURRY" } } let(:test_resource) { ::Test::Resource.new(values) } it "should memoize after bool values change " do test_resource.encode expect(test_resource.instance_variable_get(:@encode)).to eq(test_resource.encode) test_resource.ext_is_searchable = false expect(test_resource.instance_variable_get(:@encode)).to be_nil end end context "string" do let(:values) { { :ext_is_searchable => true, :name => "STEPH CURRY" } } let(:test_resource) { ::Test::Resource.new(values) } it "should memoize after bool values change " do test_resource.encode expect(test_resource.instance_variable_get(:@encode)).to eq(test_resource.encode) test_resource.name = "MVP" expect(test_resource.instance_variable_get(:@encode)).to be_nil end end context "string" do let(:values) { { :ext_is_searchable => true, :name => "STEPH CURRY" } } let(:test_resource) { ::Test::Resource.new(values) } it "should memoize after string values change " do test_resource.encode expect(test_resource.instance_variable_get(:@encode)).to eq(test_resource.encode) test_resource.name = "MVP" expect(test_resource.instance_variable_get(:@encode)).to be_nil end end context "Int64" do let(:values) { { :name => "STEPH CURRY", :date_created => 1454712125 } } let(:test_resource) { ::Test::Resource.new(values) } it "should memoize after Int64 values change " do test_resource.encode expect(test_resource.instance_variable_get(:@encode)).to eq(test_resource.encode) test_resource.date_created = 5554712127 expect(test_resource.instance_variable_get(:@encode)).to be_nil end end end context "when there's no value for a required field" do let(:message) { ::Test::ResourceWithRequiredField.new } it "raises a 'message not initialized' error" do expect do message.encode end.to raise_error(Protobuf::SerializationError, /required/i) end end context "repeated fields" do let(:message) { ::Test::Resource.new(:name => "something") } it "does not raise an error when repeated fields are []" do expect do message.repeated_enum = [] message.encode end.to_not raise_error end it "sets the value to nil when empty array is passed" do message.repeated_enum = [] expect(message.instance_variable_get("@values")[:repeated_enum]).to be_nil end it "does not compact the edit original array" do a = [nil].freeze message.repeated_enum = a expect(message.repeated_enum).to eq([]) expect(a).to eq([nil].freeze) end it "compacts the set array" do message.repeated_enum = [nil] expect(message.repeated_enum).to eq([]) end it "raises TypeError when a non-array replaces it" do expect do message.repeated_enum = 2 end.to raise_error(/value of type/) end end end describe "boolean predicate methods" do subject { Test::ResourceFindRequest.new(:name => "resource") } it { is_expected.to respond_to(:active?) } it "sets the predicate to true when the boolean value is true" do subject.active = true expect(subject.active?).to be true end it "sets the predicate to false when the boolean value is false" do subject.active = false expect(subject.active?).to be false end it "does not put predicate methods on non-boolean fields" do expect(Test::ResourceFindRequest.new(:name => "resource")).to_not respond_to(:name?) end end describe "#respond_to_and_has?" do subject { Test::EnumTestMessage.new(:non_default_enum => 2) } it "is false when the message does not have the field" do expect(subject.respond_to_and_has?(:other_field)).to be false end it "is true when the message has the field" do expect(subject.respond_to_and_has?(:non_default_enum)).to be true end end describe "#respond_to_has_and_present?" do subject { Test::EnumTestMessage.new(:non_default_enum => 2) } it "is false when the message does not have the field" do expect(subject.respond_to_and_has_and_present?(:other_field)).to be false end it "is false when the field is repeated and a value is not present" do expect(subject.respond_to_and_has_and_present?(:repeated_enums)).to be false end it "is false when the field is repeated and the value is empty array" do subject.repeated_enums = [] expect(subject.respond_to_and_has_and_present?(:repeated_enums)).to be false end it "is true when the field is repeated and a value is present" do subject.repeated_enums = [2] expect(subject.respond_to_and_has_and_present?(:repeated_enums)).to be true end it "is true when the message has the field" do expect(subject.respond_to_and_has_and_present?(:non_default_enum)).to be true end context "#API" do subject { Test::EnumTestMessage.new(:non_default_enum => 2) } specify { expect(subject).to respond_to(:respond_to_and_has_and_present?) } specify { expect(subject).to respond_to(:responds_to_and_has_and_present?) } specify { expect(subject).to respond_to(:responds_to_has?) } specify { expect(subject).to respond_to(:respond_to_has?) } specify { expect(subject).to respond_to(:respond_to_has_present?) } specify { expect(subject).to respond_to(:responds_to_has_present?) } specify { expect(subject).to respond_to(:respond_to_and_has_present?) } specify { expect(subject).to respond_to(:responds_to_and_has_present?) } end end describe '#inspect' do let(:klass) do Class.new(Protobuf::Message) do |klass| enum_class = Class.new(Protobuf::Enum) do define :YAY, 1 end klass.const_set(:EnumKlass, enum_class) optional :string, :name, 1 repeated :int32, :counts, 2 optional enum_class, :enum, 3 end end before { stub_const('MyMessage', klass) } it 'lists the fields' do proto = klass.new(:name => 'wooo', :counts => [1, 2, 3], :enum => klass::EnumKlass::YAY) expect(proto.inspect).to eq('#>') end end describe '#to_hash' do context 'generating values for an ENUM field' do it 'converts the enum to its tag representation' do hash = Test::EnumTestMessage.new(:non_default_enum => :TWO).to_hash expect(hash).to eq(:non_default_enum => 2) end it 'does not populate default values' do hash = Test::EnumTestMessage.new.to_hash expect(hash).to eq({}) end it 'converts repeated enum fields to an array of the tags' do hash = Test::EnumTestMessage.new(:repeated_enums => [:ONE, :TWO, :TWO, :ONE]).to_hash expect(hash).to eq(:repeated_enums => [1, 2, 2, 1]) end end context 'generating values for a Message field' do it 'recursively hashes field messages' do hash = Test::Nested.new(:resource => { :name => 'Nested' }).to_hash expect(hash).to eq(:resource => { :name => 'Nested' }) end it 'recursively hashes a repeated set of messages' do proto = Test::Nested.new( :multiple_resources => [ Test::Resource.new(:name => 'Resource 1'), Test::Resource.new(:name => 'Resource 2'), ], ) expect(proto.to_hash).to eq( :multiple_resources => [ { :name => 'Resource 1' }, { :name => 'Resource 2' }, ], ) end end end describe '#to_json' do subject do ::Test::ResourceFindRequest.new(:name => 'Test Name', :active => false) end specify { expect(subject.to_json).to eq '{"name":"Test Name","active":false}' } end describe '.to_json' do it 'returns the class name of the message for use in json encoding' do expect do ::Timeout.timeout(0.1) do expect(::Test::Resource.to_json).to eq("Test::Resource") end end.not_to raise_error end end describe "#define_setter" do subject { ::Test::Resource.new } it "allows string fields to be set to nil" do expect { subject.name = nil }.to_not raise_error end it "does not allow string fields to be set to Numeric" do expect { subject.name = 1 }.to raise_error(/name/) end end describe '.get_extension_field' do it 'fetches an extension field by its tag' do field = ::Test::Resource.get_extension_field(100) expect(field).to be_a(::Protobuf::Field::BoolField) expect(field.tag).to eq(100) expect(field.name).to eq(:ext_is_searchable) expect(field).to be_extension end it 'fetches an extension field by its symbolized name' do expect(::Test::Resource.get_extension_field(:ext_is_searchable)).to be_a(::Protobuf::Field::BoolField) expect(::Test::Resource.get_extension_field('ext_is_searchable')).to be_a(::Protobuf::Field::BoolField) end it 'returns nil when attempting to get a non-extension field' do expect(::Test::Resource.get_extension_field(1)).to be_nil end it 'returns nil when field is not found' do expect(::Test::Resource.get_extension_field(-1)).to be_nil expect(::Test::Resource.get_extension_field(nil)).to be_nil end end describe '.get_field' do it 'fetches a non-extension field by its tag' do field = ::Test::Resource.get_field(1) expect(field).to be_a(::Protobuf::Field::StringField) expect(field.tag).to eq(1) expect(field.name).to eq(:name) expect(field).not_to be_extension end it 'fetches a non-extension field by its symbolized name' do expect(::Test::Resource.get_field(:name)).to be_a(::Protobuf::Field::StringField) expect(::Test::Resource.get_field('name')).to be_a(::Protobuf::Field::StringField) end it 'fetches an extension field when forced' do expect(::Test::Resource.get_field(100, true)).to be_a(::Protobuf::Field::BoolField) expect(::Test::Resource.get_field(:ext_is_searchable, true)).to be_a(::Protobuf::Field::BoolField) expect(::Test::Resource.get_field('ext_is_searchable', true)).to be_a(::Protobuf::Field::BoolField) end it 'returns nil when attempting to get an extension field' do expect(::Test::Resource.get_field(100)).to be_nil end it 'returns nil when field is not defined' do expect(::Test::Resource.get_field(-1)).to be_nil expect(::Test::Resource.get_field(nil)).to be_nil end end end