Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/ldclient-rb/impl/data_system/polling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,15 @@ def self.polling_payload_to_changeset(data)
builder.start(server_intent.payload.code)

when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT
unless event[:data].is_a?(Hash)
return LaunchDarkly::Result.fail("Invalid payload: 'put-object' data must be an object")
end
unless event[:data][:kind].is_a?(String)
return LaunchDarkly::Result.fail("Invalid payload: 'put-object' kind must be a string")
end

next if LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(event[:data])

begin
put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(event[:data])
rescue ArgumentError => e
Expand All @@ -506,6 +515,15 @@ def self.polling_payload_to_changeset(data)
builder.add_put(put.kind, put.key, put.version, put.object)

when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT
unless event[:data].is_a?(Hash)
return LaunchDarkly::Result.fail("Invalid payload: 'delete-object' data must be an object")
end
unless event[:data][:kind].is_a?(String)
return LaunchDarkly::Result.fail("Invalid payload: 'delete-object' kind must be a string")
end

next if LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(event[:data])

begin
delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(event[:data])
rescue ArgumentError => e
Expand Down
18 changes: 18 additions & 0 deletions lib/ldclient-rb/impl/data_system/protocolv2.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'json'
require 'ldclient-rb/interfaces/data_system'

module LaunchDarkly
module Impl
Expand All @@ -9,6 +10,23 @@ module ProtocolV2
# LaunchDarkly data system version 2 (FDv2).
#

SUPPORTED_KINDS = [
LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG,
LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT,
].freeze

#
# Returns true if put-object or delete-object data names a kind this SDK
# does not support. Such objects are ignored so that payloads carrying
# newer kinds can still be applied.
#
# @param data [Hash] The hash representation of the event data, with a String kind
# @return [Boolean]
#
def self.unsupported_kind?(data)
!SUPPORTED_KINDS.include?(data[:kind])
end

#
# DeleteObject specifies the deletion of a particular object.
#
Expand Down
20 changes: 16 additions & 4 deletions lib/ldclient-rb/impl/data_system/streaming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -246,13 +246,25 @@ def stop
nil

when LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT
put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(JSON.parse(message.data, symbolize_names: true))
change_set_builder.add_put(put.kind, put.key, put.version, put.object)
data = JSON.parse(message.data, symbolize_names: true)
raise ArgumentError, "put-object data must be an object" unless data.is_a?(Hash)
raise ArgumentError, "put-object kind must be a string" unless data[:kind].is_a?(String)

unless LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(data)
put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.from_h(data)
change_set_builder.add_put(put.kind, put.key, put.version, put.object)
end
nil

when LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT
delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(JSON.parse(message.data, symbolize_names: true))
change_set_builder.add_delete(delete_object.kind, delete_object.key, delete_object.version)
data = JSON.parse(message.data, symbolize_names: true)
raise ArgumentError, "delete-object data must be an object" unless data.is_a?(Hash)
raise ArgumentError, "delete-object kind must be a string" unless data[:kind].is_a?(String)

unless LaunchDarkly::Impl::DataSystem::ProtocolV2.unsupported_kind?(data)
delete_object = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.from_h(data)
change_set_builder.add_delete(delete_object.kind, delete_object.key, delete_object.version)
end
nil

when LaunchDarkly::Interfaces::DataSystem::EventName::GOODBYE
Expand Down
46 changes: 46 additions & 0 deletions spec/impl/data_system/polling_payload_parsing_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,28 @@ module DataSystem
expect(result.value.selector.version).to eq(461)
end

it "ignores put and delete objects of unknown kinds" do
payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {"key":"sampleflag","kind":"flag","version":461,"object":{"key":"sampleflag","version":112}}},{"event": "put-object","data": {"key":"sampleskill","kind":"some-future-kind","version":461,"object":{"foo":1}}},{"event": "delete-object","data": {"key":"sampleskill","kind":"some-future-kind","version":461}},{"event": "delete-object","data": {"key":"samplesegment","kind":"segment","version":461}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength

# Parse a payload that mixes known and unknown object kinds.
result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true))

# The payload completes with only the known objects.
expect(result.success?).to eq(true)
expect(result.value.intent_code).to eq(LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL)
expect(result.value.changes.length).to eq(2)

expect(result.value.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT)
expect(result.value.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG)
expect(result.value.changes[0].key).to eq(:sampleflag)

expect(result.value.changes[1].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE)
expect(result.value.changes[1].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT)
expect(result.value.changes[1].key).to eq(:samplesegment)

expect(result.value.selector.state).to eq("(p:5A46PZ79FQ9D08YYKT79DECDNV:461)")
end

it "handles invalid put object" do
payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "put-object","data": {}},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength
result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true))
Expand All @@ -112,6 +134,30 @@ module DataSystem
expect(result.success?).to eq(false)
end

it "handles put and delete objects whose data is not an object" do
%w[put-object delete-object].each do |event_name|
['"x"', "[]", "null", "5"].each do |data|
payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "' + event_name + '","data": ' + data + '},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength
result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true))
expect(result.success?).to eq(false)
expect(result.error).to eq("Invalid payload: '#{event_name}' data must be an object")
end
end
end

it "handles put and delete objects whose kind is not a string" do
%w[put-object delete-object].each do |event_name|
object_field = event_name == "put-object" ? ',"object":{}' : ""
["", '"kind":null,', '"kind":5,', '"kind":["flag"],'].each do |kind_field|
data = '{' + kind_field + '"key":"sampleflag","version":461' + object_field + '}'
payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event": "' + event_name + '","data": ' + data + '},{"event":"payload-transferred","data":{"state":"(p:5A46PZ79FQ9D08YYKT79DECDNV:461)","id":"5A46PZ79FQ9D08YYKT79DECDNV","version":461}}]}' # rubocop:disable Layout/LineLength
result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true))
expect(result.success?).to eq(false)
expect(result.error).to eq("Invalid payload: '#{event_name}' kind must be a string")
end
end
end

it "handles invalid payload transferred" do
payload_str = '{"events":[ {"event":"server-intent","data":{"payloads":[ {"id":"5A46PZ79FQ9D08YYKT79DECDNV","target":461,"intentCode":"xfer-full","reason":"payload-missing"}]}},{"event":"payload-transferred","data":{}}]}' # rubocop:disable Layout/LineLength
result = LaunchDarkly::Impl::DataSystem.polling_payload_to_changeset(JSON.parse(payload_str, symbolize_names: true))
Expand Down
94 changes: 94 additions & 0 deletions spec/impl/data_system/streaming_synchronizer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,100 @@ def initialize(type, data = nil)
expect(update.change_set.changes[0].version).to eq(101)
end

it "ignores put and delete objects of unknown kinds" do
server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.new(
payload: LaunchDarkly::Interfaces::DataSystem::Payload.new(
id: "id",
target: 300,
code: LaunchDarkly::Interfaces::DataSystem::IntentCode::TRANSFER_FULL,
reason: "cant-catchup"
)
)
known_put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.new(
version: 100,
kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG,
key: "flagkey",
object: { key: "flagkey" }
)
unknown_put = LaunchDarkly::Impl::DataSystem::ProtocolV2::PutObject.new(
version: 100,
kind: "some-future-kind",
key: "otherkey",
object: { foo: 1 }
)
unknown_delete = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.new(
version: 101,
kind: "some-future-kind",
key: "otherkey"
)
known_delete = LaunchDarkly::Impl::DataSystem::ProtocolV2::DeleteObject.new(
version: 101,
kind: LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT,
key: "segmentkey"
)
selector = LaunchDarkly::Interfaces::DataSystem::Selector.new(state: "p:SOMETHING:300", version: 300)

expect(logger).not_to receive(:warn)
expect(logger).not_to receive(:error)

# Process a payload that mixes known and unknown object kinds
synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::SERVER_INTENT, JSON.generate(server_intent.to_h)),
change_set_builder, envid)
[known_put, unknown_put].each do |put|
result = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT, JSON.generate(put.to_h)),
change_set_builder, envid)
expect(result).to be_nil
end
[unknown_delete, known_delete].each do |delete_object|
result = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT, JSON.generate(delete_object.to_h)),
change_set_builder, envid)
expect(result).to be_nil
end
update = synchronizer.send(:process_message, MockSSEEvent.new(LaunchDarkly::Interfaces::DataSystem::EventName::PAYLOAD_TRANSFERRED, JSON.generate(selector.to_h)),
change_set_builder, envid)

# The payload completes as valid with only the known objects
expect(update).not_to be_nil
expect(update.state).to eq(LaunchDarkly::Interfaces::DataSource::Status::VALID)
expect(update.error).to be_nil
expect(update.change_set).not_to be_nil
expect(update.change_set.changes.length).to eq(2)
expect(update.change_set.changes[0].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::PUT)
expect(update.change_set.changes[0].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::FLAG)
expect(update.change_set.changes[0].key).to eq(:flagkey)
expect(update.change_set.changes[1].action).to eq(LaunchDarkly::Interfaces::DataSystem::ChangeType::DELETE)
expect(update.change_set.changes[1].kind).to eq(LaunchDarkly::Interfaces::DataSystem::ObjectKind::SEGMENT)
expect(update.change_set.changes[1].key).to eq(:segmentkey)
end

it "rejects put and delete objects whose data is not an object" do
[
LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT,
LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT,
].each do |event_name|
['"x"', "[]", "null", "5"].each do |data|
expect {
synchronizer.send(:process_message, MockSSEEvent.new(event_name, data), change_set_builder, envid)
}.to raise_error(ArgumentError, "#{event_name} data must be an object")
end
end
end

it "rejects put and delete objects whose kind is not a string" do
[
LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT,
LaunchDarkly::Interfaces::DataSystem::EventName::DELETE_OBJECT,
].each do |event_name|
object_field = event_name == LaunchDarkly::Interfaces::DataSystem::EventName::PUT_OBJECT ? ',"object":{}' : ""
["", '"kind":null,', '"kind":5,', '"kind":["flag"],'].each do |kind_field|
data = '{' + kind_field + '"key":"flagkey","version":100' + object_field + '}'
expect {
synchronizer.send(:process_message, MockSSEEvent.new(event_name, data), change_set_builder, envid)
}.to raise_error(ArgumentError, "#{event_name} kind must be a string")
end
end
end

it "swallows goodbye events" do
server_intent = LaunchDarkly::Interfaces::DataSystem::ServerIntent.new(
payload: LaunchDarkly::Interfaces::DataSystem::Payload.new(
Expand Down
Loading