diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index 5565c3c2..db511600 100644 --- a/Gemfile +++ b/Gemfile @@ -5,5 +5,7 @@ source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "cucumber" +gem "rspec" gem "rspec-expectations" +gem "timecop" gem "colorize" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 13f664c0..ab1e0b2b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,5 +1,5 @@ GEM - connection: https://rubygems.org/ + remote: https://rubygems.org/ specs: backports (3.15.0) builder (3.2.3) @@ -24,10 +24,20 @@ GEM gherkin (5.1.0) multi_json (1.13.1) multi_test (0.1.2) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.2) + rspec-support (~> 3.8.0) rspec-expectations (3.8.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.8.0) + rspec-mocks (3.8.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) rspec-support (3.8.0) + timecop (0.9.1) PLATFORMS ruby @@ -35,7 +45,9 @@ PLATFORMS DEPENDENCIES colorize cucumber + rspec rspec-expectations + timecop BUNDLED WITH 2.0.1 diff --git a/connector.rb b/connector.rb index 0cf58c44..b40e0263 100644 --- a/connector.rb +++ b/connector.rb @@ -504,6 +504,7 @@ def wait_for_acknowledgement original, timeout, options={} end def wait_for_not_acknowledged original, timeout + p :not wait_for_acknowledgement original, timeout, type: :not_acknowledged end diff --git a/message.rb b/message.rb index 4947dcf0..178e695e 100644 --- a/message.rb +++ b/message.rb @@ -1,7 +1,9 @@ # rsmp messages require 'json' -require 'securerandom' +require'securerandom' +require_relative 'error' +require_relative 'rsmp' module RSMP class Message @@ -10,6 +12,7 @@ class Message attr_accessor :json, :direction, def self.parse_attributes packet + raise ArgumentError unless packet JSON.parse packet rescue JSON::ParserError raise InvalidPacket, bin_to_chars(packet) @@ -53,9 +56,6 @@ def self.build attributes, packet message end - def validate - end - def type @attributes["type"] end @@ -106,8 +106,8 @@ def initialize attributes = {} end def validate - validate_type && - validate_id + validate_type == true && + validate_id == true end def validate_type @@ -115,7 +115,7 @@ def validate_type end def validate_id - @attributes["mId"] =~ /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}/i + (@attributes["mId"] =~ /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}/i) != nil end def valid? diff --git a/spec/message_spec.rb b/spec/message_spec.rb new file mode 100644 index 00000000..d18f3079 --- /dev/null +++ b/spec/message_spec.rb @@ -0,0 +1,180 @@ +require_relative '../message' +require 'timecop' + +def build packet + attributes = RSMP::Message.parse_attributes(packet) + RSMP::Message.build(attributes,packet) +end + +describe RSMP::Message do + let(:version_str) { '{"mType":"rSMsg","type":"Version","RSMP":[{"vers":"3.1.1"},{"vers":"3.1.2"},{"vers":"3.1.3"},{"vers":"3.1.4"}],"siteId":[{"sId":"RN+SI0001"}],"SXL":"1.1","mId":"8db00f0a-4124-406f-b3f9-ceb0dbe4aeb6"}' } + let(:ack_str) { '{"mType":"rSMsg","type":"MessageAck","oMId":"a54dc38b-7ddb-42a6-b6e8-95b0d00dad19","mId":"561c15c9-e050-4ee7-9cf4-8643c6769dcb"}' } + let(:not_ack_str) { '{"mType":"rSMsg","type":"MessageNotAck","rea":"since we are a rsmp::siteconnector","oMId":"24b5e2d1-fd32-4f12-80cf-f32f8b2772af","mId":"808b957d-6e93-408b-b5e3-ce7f64dc3c61"}' } + let(:watchdog_str) { '{"mType":"rSMsg","type":"Watchdog","wTs":"2019-07-11 06:37:55 UTC","mId":"a8cafa58-31bc-40bb-b335-645b5ac985cd"}' } + let(:command_request_str) { '{"mType":"rSMsg","type":"CommandRequest","ntsOId":"","xNId":"","cId":"AA+BBCCC=DDDEE002","arg":[{"cCI":"MA104","n":"message","cO":"","v":"Rainbbows!"}],"mId":"1a913af3-82ba-489b-8895-54c2fb56d728"}' } + let(:command_response_str) { '{"mType":"rSMsg","type":"CommandResponse","cId":"AA+BBCCC=DDDEE002","cTS":"2019-07-11T06:37:55.914Z","rvs":[{"cCI":"MA104","n":"message","v":"Rainbbows!","age":"recent"}],"mId":"f0f38584-e3ff-46f8-88a1-598e7de0e671"}' } + let(:aggregated_status_str) { ' {"mType":"rSMsg","type":"AggregatedStatus","aSTS":"2019-07-11T06:37:55.913Z","fP":null,"fS":null,"se":[false,false,false,false,false,false,false,false],"mId":"d9a904cc-b39d-4b72-ad67-f7d634552d36"}' } + let(:status_request_str) { '{"mType":"rSMsg","type":"StatusRequest","ntsOId":"","xNId":"","cId":"AA+BBCCC=DDDEE002","sS":[{"cCI":"S001","n":"number"}],"mId":"859e189e-c973-4b40-90c4-45a7a25f2dda"}' } + let(:status_response_str) { '{"mType":"rSMsg","type":"StatusResponse","cId":"AA+BBCCC=DDDEE002","sTs":"2019-07-11T06:37:56.096Z","sS":[{"cCI":"S001","n":"number","s":90,"q":"recent"}],"mId":"0872f9f4-caee-4495-96ef-68a5cf56c993"}' } + let(:status_subscribe_str) { '{"mType":"rSMsg","type":"StatusSubscribe","ntsOId":"","xNId":"","cId":"AA+BBCCC=DDDEE002","sS":[{"sCI":"S001","n":"number","uRt":"0.1"}],"mId":"6aee9e40-c6cb-4cd8-8b7a-3ee8906043c9"}' } + let(:status_unsubscribe_str) { '{"mType":"rSMsg","type":"StatusUnsubscribe","ntsOId":"","xNId":"","cId":"AA+BBCCC=DDDEE002","sS":[{"sCI":"S001","n":"number"}],"mId":"bae361e1-7b26-48f3-9776-5aac815544da"}' } + let(:status_update_str) { '{"mType":"rSMsg","type":"StatusUpdate","cId":"AA+BBCCC=DDDEE002","sTs":"2019-07-11T06:37:56.103Z","sS":[{"sCI":"S001","n":"number","s":98,"q":"recent"}],"mId":"e0694101-4b8c-4832-9bd4-7ed598b247bd"}' } + let(:unkown_str) { '{"mType":"rSMsg","type":"SomeNonExistingMessage","mId":"c014bd2d-5671-4a19-b37e-50deef301b82"}' } + let(:malformed_str) { '{"mType":"rSMsg",mId":"c014bd2d-5671-4a19-b37e-50deef301b82"}' } + + context 'when parsing json packages' do + it 'raises ArgumentError when parsing nil' do + expect { RSMP::Message.parse_attributes(nil) }.to raise_error(ArgumentError) + end + + it 'raises InvalidPacket when parsing empty string' do + expect { RSMP::Message.parse_attributes('') }.to raise_error(RSMP::InvalidPacket) + end + + it 'raises InvalidPacket when parsing whitespace' do + expect { RSMP::Message.parse_attributes(' ') }.to raise_error(RSMP::InvalidPacket) + expect { RSMP::Message.parse_attributes("\t") }.to raise_error(RSMP::InvalidPacket) + expect { RSMP::Message.parse_attributes("\n") }.to raise_error(RSMP::InvalidPacket) + expect { RSMP::Message.parse_attributes("\f") }.to raise_error(RSMP::InvalidPacket) + expect { RSMP::Message.parse_attributes("\r") }.to raise_error(RSMP::InvalidPacket) + end + + it 'raises InvalidPacket when parsing invalid JSON ' do + expect { RSMP::Message.parse_attributes('{"a":"1"') }.to raise_error(RSMP::InvalidPacket) + expect { RSMP::Message.parse_attributes('"a":"1"}') }.to raise_error(RSMP::InvalidPacket) + expect { RSMP::Message.parse_attributes('/') }.to raise_error(RSMP::InvalidPacket) + end + + it 'parses valid JSON' do + expect(RSMP::Message.parse_attributes('"string"')).to eq("string") + expect(RSMP::Message.parse_attributes('123')).to eq(123) + expect(RSMP::Message.parse_attributes('3.14')).to eq(3.14) + expect(RSMP::Message.parse_attributes('[1,2,3]')).to eq([1,2,3]) + expect(RSMP::Message.parse_attributes('{"a":"1","b":"2"}')).to eq({"a"=>"1","b"=>"2"}) + end + end + + context 'when creating messages' do + let(:json) {{ + "RSMP" => [{"vers"=>"3.1.1"}, {"vers"=>"3.1.2"}, {"vers"=>"3.1.3"}, {"vers"=>"3.1.4"}], + "SXL" => "1.1", + "mId" => "8db00f0a-4124-406f-b3f9-ceb0dbe4aeb6", + "mType" => "rSMsg", + "siteId" => [{"sId"=>"RN+SI0001"}], + "type" => "Version", + }} + + it 'builds right type of objects when parsing JSON' do + expect(build(version_str)).to be_instance_of(RSMP::Version) + expect(build(ack_str)).to be_instance_of(RSMP::MessageAck) + expect(build(not_ack_str)).to be_instance_of(RSMP::MessageNotAck) + expect(build(watchdog_str)).to be_instance_of(RSMP::Watchdog) + expect(build(command_request_str)).to be_instance_of(RSMP::CommandRequest) + expect(build(command_response_str)).to be_instance_of(RSMP::CommandResponse) + expect(build(aggregated_status_str)).to be_instance_of(RSMP::AggregatedStatus) + expect(build(status_request_str)).to be_instance_of(RSMP::StatusRequest) + expect(build(status_response_str)).to be_instance_of(RSMP::StatusResponse) + expect(build(status_subscribe_str)).to be_instance_of(RSMP::StatusSubscribe) + expect(build(status_unsubscribe_str)).to be_instance_of(RSMP::StatusUnsubscribe) + expect(build(status_update_str)).to be_instance_of(RSMP::StatusUpdate) + expect(build(unkown_str)).to be_instance_of(RSMP::Unknown) + end + + it 'parses attributes values' do + message = build(version_str) + expect(message.attributes).to eq(json) + end + + it 'initializes attributes' do + message = RSMP::Version.new json + expect(message.attributes).to eq(json) + expect(message.m_id).to eq(json["mId"]) + end + + it 'initializes timestamp' do + time = Time.new(2019, 9, 1, 14, 24, 17) + Timecop.freeze(time) do + message = RSMP::Version.new json + expect(message.timestamp).to eq(time) + end + end + + it 'randomizes message id if attributes are empty' do + message = RSMP::Version.new + expect(message.m_id).to match(/[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}/i) + another_message = RSMP::Version.new + expect(message.m_id).not_to eq(another_message.m_id) + end + + it 'builds specific message types' do + expect(RSMP::Version.new.type).to eq("Version") + expect(RSMP::MessageAck.new.type).to eq("MessageAck") + expect(RSMP::MessageNotAck.new.type).to eq("MessageNotAck") + expect(RSMP::AggregatedStatus.new.type).to eq("AggregatedStatus") + expect(RSMP::Watchdog.new.type).to eq("Watchdog") + expect(RSMP::Alarm.new.type).to eq("Alarm") + expect(RSMP::CommandRequest.new.type).to eq("CommandRequest") + expect(RSMP::CommandResponse.new.type).to eq("CommandResponse") + expect(RSMP::StatusRequest.new.type).to eq("StatusRequest") + expect(RSMP::StatusResponse.new.type).to eq("StatusResponse") + expect(RSMP::StatusSubscribe.new.type).to eq("StatusSubscribe") + expect(RSMP::StatusUnsubscribe.new.type).to eq("StatusUnsubscribe") + expect(RSMP::StatusUpdate.new.type).to eq("StatusUpdate") + expect(RSMP::Unknown.new.type).to be_nil + expect(RSMP::Malformed.new.type).to be_nil + expect(RSMP::Unknown.new.type).to be_nil + expect(RSMP::Message.new.type).to be_nil + end + + it 'generates json' do + message = RSMP::Version.new(json) + message.generate_json + str = '{"mType":"rSMsg","type":"Version","RSMP":[{"vers":"3.1.1"},{"vers":"3.1.2"},{"vers":"3.1.3"},{"vers":"3.1.4"}],"SXL":"1.1","mId":"8db00f0a-4124-406f-b3f9-ceb0dbe4aeb6","siteId":[{"sId":"RN+SI0001"}]}' + expect(message.json).to eq(str) + expect(message.out).to eq("#{str}\f") + end + + it 'validates mType' do + message = RSMP::Message.new "mType"=>"rSMsg","type"=>"Version","mId"=>"c014bd2d-5671-4a19-b37e-50deef301b82" + expect(message.validate_type).to eq(true) + + message = RSMP::Message.new "mType"=>"rBad","type"=>"Version","mId"=>"c014bd2d-5671-4a19-b37e-50deef301b82" + expect(message.validate_type).to eq(false) + end + + it 'validates message id format' do + expect(RSMP::Message.new("mType"=>"rSMsg","type"=>"Version","mId"=>"c014bd2d-5671-4a19-b37e-50deef301b82").validate_id).to eq(true) + expect(RSMP::Message.new("mType"=>"rSMsg","type"=>"Version","mId"=>"0c014bd2d-5671-4a19-b37e-50deef301b82").validate_id).to eq(true) + expect(RSMP::Message.new("mType"=>"rSMsg","type"=>"Version","mId"=>".014bd2d-5671-4a19-b37e").validate_id).to eq(false) + expect(RSMP::Message.new("mType"=>"rSMsg","type"=>"Version","mId"=>"014bd2d-5671-4a19-b37e-50deef301b82").validate_id).to eq(false) + expect(RSMP::Message.new("mType"=>"rSMsg","type"=>"Version","mId"=>"14bd2d-5671-4a19-b37e-50deef301b82").validate_id).to eq(false) + expect(RSMP::Message.new("mType"=>"rSMsg","type"=>"Version","mId"=>"c014bd2d5671-4a19-b37e-50deef301b82").validate_id).to eq(false) + expect(RSMP::Message.new("mType"=>"rSMsg","type"=>"Version","mId"=>"c014bd2d5671-4a19-037e-50deef301b82").validate_id).to eq(false) + end + end + + context 'when accessing attributes' do + let(:message) { build(version_str) } + + it 'returns attribute values' do + expect( message.attribute("SXL") ).to eq('1.1') + end + + it 'raises MissingAttribute when accessing non-existing attribute' do + expect { message.attribute("bad") }.to raise_error(RSMP::MissingAttribute, "missing attribute 'bad'") + end + + it 'raises MissingAttribute when accessing attribute with wrong case' do + expect { message.attribute("sxl") }.to raise_error(RSMP::MissingAttribute, "attribute 'SXL' should be named 'sxl'") + end + + it 'returns type' do + expect(message.type).to eq('Version') + end + + it 'returns message id' do + expect(message.m_id).to eq('8db00f0a-4124-406f-b3f9-ceb0dbe4aeb6') + end + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..251aa510 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,100 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end