require 'aws-sdk-s3' require 'aws-sdk-sqs' require 'aws-sdk-sqs/queue_poller' require 'fluent/test' require 'fluent/test/helpers' require 'fluent/test/log' require 'fluent/test/driver/input' require 'fluent/plugin/in_s3' require 'test/unit/rr' require 'zlib' require 'fileutils' require 'ostruct' include Fluent::Test::Helpers class S3InputTest < Test::Unit::TestCase def setup Fluent::Test.setup @time = event_time("2015-09-30 13:14:15 UTC") Fluent::Engine.now = @time if Fluent.const_defined?(:EventTime) stub(Fluent::EventTime).now { @time } end end CONFIG = %[ aws_key_id test_key_id aws_sec_key test_sec_key s3_bucket test_bucket buffer_type memory queue_name test_queue queue_owner_aws_account_id 123456789123 ] def create_driver(conf = CONFIG) Fluent::Test::Driver::Input.new(Fluent::Plugin::S3Input).configure(conf) end class ConfigTest < self def test_default d = create_driver extractor = d.instance.instance_variable_get(:@extractor) actual = { aws_key_id: d.instance.aws_key_id, aws_sec_key: d.instance.aws_sec_key, s3_bucket: d.instance.s3_bucket, s3_region: d.instance.s3_region, sqs_queue_name: d.instance.sqs.queue_name, extractor_ext: extractor.ext, extractor_content_type: extractor.content_type } expected = { aws_key_id: "test_key_id", aws_sec_key: "test_sec_key", s3_bucket: "test_bucket", s3_region: "us-east-1", sqs_queue_name: "test_queue", extractor_ext: "gz", extractor_content_type: "application/x-gzip" } assert_equal(expected, actual) end def test_empty assert_raise(Fluent::ConfigError) do create_driver("") end end def test_without_sqs_section conf = %[ aws_key_id test_key_id aws_sec_key test_sec_key s3_bucket test_bucket ] assert_raise_message("'' sections are required") do create_driver(conf) end end def test_unknown_store_as config = CONFIG + "\nstore_as unknown" assert_raise(Fluent::NotFoundPluginError) do create_driver(config) end end data("json" => ["json", "json", "application/json"], "text" => ["text", "txt", "text/plain"], "gzip" => ["gzip", "gz", "application/x-gzip"], "gzip_command" => ["gzip_command", "gz", "application/x-gzip"], "lzo" => ["lzo", "lzo", "application/x-lzop"], "lzma2" => ["lzma2", "xz", "application/x-xz"]) def test_extractor(data) store_type, ext, content_type = data config = CONFIG + "\nstore_as #{store_type}\n" d = create_driver(config) extractor = d.instance.instance_variable_get(:@extractor) expected = { ext: ext, content_type: content_type } actual = { ext: extractor.ext, content_type: extractor.content_type } assert_equal(expected, actual) rescue Fluent::ConfigError => e pend(e.message) end end data('Normal endpoint' => 'riak-cs.example.com', 'VPCE endpoint' => 'vpce.amazonaws.com', 'FIPS endpoint' => 'fips.xxx.amazonaws.com', 'GOV endpoint' => 'gov.xxx.amazonaws.com') def test_s3_endpoint_with_valid_endpoint(endpoint) d = create_driver(CONFIG + "s3_endpoint #{endpoint}") assert_equal endpoint, d.instance.s3_endpoint end data('US West (Oregon)' => 's3-us-west-2.amazonaws.com', 'EU (Frankfurt)' => 's3.eu-central-1.amazonaws.com', 'Asia Pacific (Tokyo)' => 's3-ap-northeast-1.amazonaws.com', 'Invalid VPCE' => 'vpce.xxx.amazonaws.com') def test_s3_endpoint_with_invalid_endpoint(endpoint) assert_raise(Fluent::ConfigError, "s3_endpoint parameter is not supported, use s3_region instead. This parameter is for S3 compatible services") { create_driver(CONFIG + "s3_endpoint #{endpoint}") } end data('US West (Oregon)' => 's3-us-west-2.amazonaws.com', 'EU (Frankfurt)' => 's3.eu-central-1.amazonaws.com', 'Asia Pacific (Tokyo)' => 's3-ap-northeast-1.amazonaws.com') def test_sqs_endpoint_with_invalid_endpoint(endpoint) assert_raise(Fluent::ConfigError, "sqs.endpoint parameter is not supported, use s3_region instead. This parameter is for SQS compatible services") { conf = <<"EOS" aws_key_id test_key_id aws_sec_key test_sec_key s3_bucket test_bucket buffer_type memory queue_name test_queue endpoint #{endpoint} EOS create_driver(conf) } end def test_sqs_with_invalid_keys_missing_secret_key assert_raise(Fluent::ConfigError, "sqs/aws_key_id or sqs/aws_sec_key is missing") { conf = <<"EOS" aws_key_id test_key_id aws_sec_key test_sec_key s3_bucket test_bucket buffer_type memory queue_name test_queue endpoint eu-west-1 aws_key_id sqs_test_key_id EOS create_driver(conf) } end def test_sqs_with_invalid_aws_keys_missing_key_id assert_raise(Fluent::ConfigError, "sqs/aws_key_id or sqs/aws_sec_key is missing") { conf = <<"EOS" aws_key_id test_key_id aws_sec_key test_sec_key s3_bucket test_bucket buffer_type memory queue_name test_queue endpoint eu-west-1 aws_sec_key sqs_test_sec_key EOS create_driver(conf) } end def test_sqs_with_valid_aws_keys_complete_pair conf = <<"EOS" aws_key_id test_key_id aws_sec_key test_sec_key s3_bucket test_bucket buffer_type memory queue_name test_queue endpoint eu-west-1 aws_key_id sqs_test_key_id aws_sec_key sqs_test_sec_key EOS d = create_driver(conf) assert_equal 'sqs_test_key_id', d.instance.sqs.aws_key_id assert_equal 'sqs_test_sec_key', d.instance.sqs.aws_sec_key end def test_with_invalid_aws_keys_missing_secret_key assert_raise(Fluent::ConfigError, "aws_key_id or aws_sec_key is missing") { conf = <<"EOS" aws_key_id test_key_id s3_bucket test_bucket buffer_type memory queue_name test_queue endpoint eu-west-1 EOS create_driver(conf) } end def test_with_invalid_aws_keys_missing_key_id assert_raise(Fluent::ConfigError, "aws_key_id or aws_sec_key is missing") { conf = <<"EOS" aws_sec_key test_sec_key s3_bucket test_bucket buffer_type memory queue_name test_queue endpoint eu-west-1 EOS create_driver(conf) } end def test_with_valid_aws_keys_complete_pair conf = <<"EOS" aws_key_id test_key_id aws_sec_key test_sec_key s3_bucket test_bucket buffer_type memory queue_name test_queue endpoint eu-west-1 EOS d = create_driver(conf) assert_equal 'test_key_id', d.instance.aws_key_id assert_equal 'test_sec_key', d.instance.aws_sec_key end Struct.new("StubResponse", :queue_url) Struct.new("StubMessage", :message_id, :receipt_handle, :body) def setup_mocks @s3_client = stub(Aws::S3::Client.new(stub_responses: true)) stub(@s3_client).config { OpenStruct.new({region: "us-east-1"}) } mock(Aws::S3::Client).new(anything).at_least(0) { @s3_client } @s3_resource = mock(Aws::S3::Resource.new(client: @s3_client)) mock(Aws::S3::Resource).new(client: @s3_client) { @s3_resource } @s3_bucket = mock(Aws::S3::Bucket.new(name: "test", client: @s3_client)) @s3_bucket.exists? { true } @s3_resource.bucket(anything) { @s3_bucket } test_queue_url = "http://example.com/test_queue" @sqs_client = stub(Aws::SQS::Client.new(stub_responses: true)) @sqs_response = stub(Struct::StubResponse.new(test_queue_url)) @sqs_client.get_queue_url(queue_name: "test_queue", queue_owner_aws_account_id: "123456789123"){ @sqs_response } mock(Aws::SQS::Client).new(anything).once { @sqs_client } @real_poller = Aws::SQS::QueuePoller.new(test_queue_url, client: @sqs_client) @sqs_poller = stub(@real_poller) mock(Aws::SQS::QueuePoller).new(anything, client: @sqs_client) { @sqs_poller } end def test_no_records setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\n") mock(d.instance).process(anything).never message = Struct::StubMessage.new(1, 1, "{}") @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count > 1 d.instance.instance_variable_set(:@running, false) end [message] end assert_nothing_raised do d.run {} end end def test_one_record setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\n") s3_object = stub(Object.new) s3_response = stub(Object.new) s3_response.body { StringIO.new("aaa") } s3_object.get { s3_response } @s3_bucket.object(anything).at_least(1) { s3_object } body = { "Records" => [ { "s3" => { "object" => { "key" => "test_key" } } } ] } message = Struct::StubMessage.new(1, 1, Yajl.dump(body)) @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count >= 1 d.instance.instance_variable_set(:@running, false) end [message] end d.run(expect_emits: 1) events = d.events assert_equal({ "message" => "aaa" }, events.first[2]) end def test_one_record_with_metadata setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\nadd_object_metadata true\n") s3_object = stub(Object.new) s3_response = stub(Object.new) s3_response.body { StringIO.new("aaa") } s3_object.get { s3_response } @s3_bucket.object(anything).at_least(1) { s3_object } body = { "Records" => [ { "s3" => { "object" => { "key" => "test_key" } } } ] } message = Struct::StubMessage.new(1, 1, Yajl.dump(body)) @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count >= 1 d.instance.instance_variable_set(:@running, false) end [message] end d.run(expect_emits: 1) events = d.events assert_equal({ "s3_bucket" => "test_bucket", "s3_key" => "test_key", "message" => "aaa" }, events.first[2]) end def test_one_record_url_encoded setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\n") s3_object = stub(Object.new) s3_response = stub(Object.new) s3_response.body { StringIO.new("aaa") } s3_object.get { s3_response } @s3_bucket.object('test key').at_least(1) { s3_object } body = { "Records" => [ { "s3" => { "object" => { "key" => "test+key" } } } ] } message = Struct::StubMessage.new(1, 1, Yajl.dump(body)) @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count >= 1 d.instance.instance_variable_set(:@running, false) end [message] end d.run(expect_emits: 1) events = d.events assert_equal({ "message" => "aaa" }, events.first[2]) end def test_one_record_url_encoded_with_metadata setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\nadd_object_metadata true") s3_object = stub(Object.new) s3_response = stub(Object.new) s3_response.body { StringIO.new("aaa") } s3_object.get { s3_response } @s3_bucket.object('test key').at_least(1) { s3_object } body = { "Records" => [ { "s3" => { "object" => { "key" => "test+key" } } } ] } message = Struct::StubMessage.new(1, 1, Yajl.dump(body)) @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count >= 1 d.instance.instance_variable_set(:@running, false) end [message] end d.run(expect_emits: 1) events = d.events assert_equal({ "s3_bucket" => "test_bucket", "s3_key" => "test+key", "message" => "aaa" }, events.first[2]) end def test_one_record_multi_line setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\n") s3_object = stub(Object.new) s3_response = stub(Object.new) s3_response.body { StringIO.new("aaa\nbbb\nccc\n") } s3_object.get { s3_response } @s3_bucket.object(anything).at_least(1) { s3_object } body = { "Records" => [ { "s3" => { "object" => { "key" => "test_key" } } } ] } message = Struct::StubMessage.new(1, 1, Yajl.dump(body)) @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count >= 1 d.instance.instance_variable_set(:@running, false) end [message] end d.run(expect_emits: 1) events = d.events expected_records = [ { "message" => "aaa\n" }, { "message" => "bbb\n" }, { "message" => "ccc\n" } ] assert_equal(expected_records, events.map {|_tag, _time, record| record }) end def test_one_record_multi_line_with_metadata setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\nadd_object_metadata true") s3_object = stub(Object.new) s3_response = stub(Object.new) s3_response.body { StringIO.new("aaa\nbbb\nccc\n") } s3_object.get { s3_response } @s3_bucket.object(anything).at_least(1) { s3_object } body = { "Records" => [ { "s3" => { "object" => { "key" => "test_key" } } } ] } message = Struct::StubMessage.new(1, 1, Yajl.dump(body)) @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count >= 1 d.instance.instance_variable_set(:@running, false) end [message] end d.run(expect_emits: 1) events = d.events expected_records = [ { "s3_bucket" => "test_bucket", "s3_key" => "test_key", "message" => "aaa\n" }, { "s3_bucket" => "test_bucket", "s3_key" => "test_key", "message" => "bbb\n" }, { "s3_bucket" => "test_bucket", "s3_key" => "test_key", "message" => "ccc\n" } ] assert_equal(expected_records, events.map {|_tag, _time, record| record }) end def test_gzip_single_stream setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as gzip\nformat none\n") s3_object = stub(Object.new) s3_response = stub(Object.new) s3_response.body { io = StringIO.new Zlib::GzipWriter.wrap(io) do |gz| gz.write "aaa\nbbb\n" gz.finish end io.rewind io } s3_object.get { s3_response } @s3_bucket.object(anything).at_least(1) { s3_object } body = { "Records" => [ { "s3" => { "object" => { "key" => "test_key" } } } ] } message = Struct::StubMessage.new(1, 1, Yajl.dump(body)) @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count >= 1 d.instance.instance_variable_set(:@running, false) end [message] end d.run(expect_emits: 1) events = d.events expected_records = [ { "message" => "aaa\n" }, { "message" => "bbb\n" } ] assert_equal(expected_records, events.map {|_tag, _time, record| record }) end def test_gzip_multiple_steams setup_mocks d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as gzip\nformat none\n") s3_object = stub(Object.new) s3_response = stub(Object.new) s3_response.body { io = StringIO.new Zlib::GzipWriter.wrap(io) do |gz| gz.write "aaa\nbbb\n" gz.finish end Zlib::GzipWriter.wrap(io) do |gz| gz.write "ccc\nddd\n" gz.finish end io.rewind io } s3_object.get { s3_response } @s3_bucket.object(anything).at_least(1) { s3_object } body = { "Records" => [ { "s3" => { "object" => { "key" => "test_key" } } } ] } message = Struct::StubMessage.new(1, 1, Yajl.dump(body)) @sqs_poller.get_messages(anything, anything) do |config, stats| config.before_request.call(stats) if config.before_request stats.request_count += 1 if stats.request_count >= 1 d.instance.instance_variable_set(:@running, false) end [message] end d.run(expect_emits: 1) events = d.events expected_records = [ { "message" => "aaa\n" }, { "message" => "bbb\n" }, { "message" => "ccc\n" }, { "message" => "ddd\n" } ] assert_equal(expected_records, events.map {|_tag, _time, record| record }) end end