# coding: utf-8 require 'spec_helper' require 'flydata/command/sync' module Flydata module Command describe Sync do let(:subject_object){ described_class.new } subject { subject_object } let(:default_mysqldump_dir) do File.join('/tmp', "sync_dump_#{Time.now.to_i}") end let(:default_data_entry) do {"id"=>93, "name"=>"flydata_sync_mysql", "data_port_id"=>52, "display_name"=>"flydata_sync_mysql", "enabled"=>true, "heroku_log_type"=>nil, "heroku_resource_id"=>nil, "log_deletion"=>nil, "log_file_delimiter"=>nil, "log_file_type"=>nil, "log_path"=>nil, "redshift_schema_name"=>"", "redshift_table_name"=>nil, "created_at"=>"2014-01-22T18:58:43Z", "updated_at"=>"2014-01-30T02:42:26Z", "type"=>"RedshiftMysqlDataEntry", "tag_name"=>"flydata.a458c641_dp52.flydata_mysql", "tag_name_dev"=>"flydata.a458c641_dp52.flydata_mysql.dev", "data_port_key"=>"a458c641", "mysql_data_entry_preference" => { "host"=>"localhost", "port"=>3306, "username"=>"masashi", "password"=>"welcome", "database"=>"sync_test", "tables"=>["table1", "table2", "table4"], "invalid_tables"=>["table3"], "new_tables"=>["table4"], "mysqldump_dir"=>default_mysqldump_dir, "forwarder" => "tcpforwarder", "data_servers"=>"localhost:9905" } } end let(:col4_type) { "binary" } let(:col4_width) { 34 } let(:col4_type_str) { "#{col4_type}(#{col4_width})" } let(:mysql_table_columns) { {"id"=>{:column_name=>"id", :format_type_str=>"int(11)", :format_type=>"int", :format_size=>11}, "name"=>{:column_name=>"name", :format_type_str=>"varchar(40)", :format_type=>"varchar", :format_size=>40, :default=>nil}, "created_at"=>{:column_name=>"created_at", :format_type_str=>"timestamp", :format_type=>"timestamp", :default=>"CURRENT_TIMESTAMP"}, "bin"=>{:column_name=>"bin", :format_type_str=>col4_type_str, :format_type=>col4_type, :format_size=>col4_width, :default=>nil}, "bin2"=>{:column_name=>"bin2", :format_type_str=>"blob", :format_type=>"blob"}, "varbin"=>{:column_name=>"varbin", :format_type_str=>"varchar(34)", :format_type=>"varchar", :format_size=>34, :default=>nil}} } let(:mysql_table) do Flydata::Parser::Mysql::MysqlTable.new("test_table", mysql_table_columns, ['id']) end after :each do if Dir.exists?(default_mysqldump_dir) Dir.delete(default_mysqldump_dir) rescue nil end if File.exists?(default_mysqldump_dir) File.delete(default_mysqldump_dir) rescue nil end end describe '#cleanup_sync_server' do let(:rest_client) { double('rest_client') } context "when rest client throws timeout errors" do before do allow(RestClient::Resource).to receive(:new).and_return(rest_client) end it "should retry in case of RestClient::RequestTimeout" do expect(rest_client).to receive(:post).and_raise(RestClient::RequestTimeout) expect(rest_client).to receive(:post).and_return("{}") subject.send(:cleanup_sync_server, default_data_entry) end it "should retry in case of RestClient::GatewayTimeout" do expect(rest_client).to receive(:post).and_raise(RestClient::GatewayTimeout) expect(rest_client).to receive(:post).and_return("{}") subject.send(:cleanup_sync_server, default_data_entry) end end end describe '#generate_mysqldump' do let (:flydata) { double('flydata') } let (:dp) { double('dp') } let (:default_data_port) { double('default_data_port') } let (:default_sync_fm) { double('default_sync_fm') } let (:default_dump_pos) do { status: "DO_NOT_SKIP_DUMP" } end let (:default_fp) { double('default_fp') } let (:default_backup) { double('default_backup') } let (:target_tables) { ["test_table_1"] } let (:db_byte) { 1 } let (:disk_byte) { 100 } before do expect(subject).to receive(:flydata).and_return(flydata).at_least(:once) expect(flydata).to receive(:data_port).and_return(dp) allow(flydata).to receive(:data_entry).and_return(default_data_entry) expect(dp).to receive(:get).and_return(default_data_port) allow(File).to receive(:exists?).and_return(false) expect(default_sync_fm).to receive(:load_dump_pos).and_return(default_dump_pos) expect(default_sync_fm).to receive(:dump_file_path).and_return(default_fp) expect(default_sync_fm).to receive(:backup_dir).and_return(default_backup) expect(subject).to receive(:target_tables).and_return(target_tables).at_least(:once) expect(subject).to receive(:print) expect(subject).to receive(:log_info) expect(subject).to receive(:log_info_stdout).at_least(:once) expect(subject).to receive(:ask_yes_no).and_return(true).at_least(:once) Flydata::Parser::Mysql::DatabaseSizeCheck.any_instance.should_receive(:get_db_bytesize).and_return(db_byte) Flydata::MysqlCompatibilityCheck.any_instance.should_receive(:check) end context 'with no stream option' do before do expect(default_sync_fm).to receive(:save_sync_info).once expect(subject).to receive(:free_disk_space).and_return(disk_byte) expect(File).to receive(:dirname) end it 'will export to dump file' do expect(subject).to receive(:call_block_or_return_io) Flydata::Parser::Mysql::MysqlDumpGeneratorNoMasterData.any_instance.should_receive(:dump).with(default_fp) subject.send(:generate_mysqldump, default_data_entry, default_sync_fm) end it 'will remove dump file on interrupt' do expect(default_sync_fm).to receive(:delete_dump_file) expect { Flydata::Parser::Mysql::MysqlDumpGeneratorNoMasterData.any_instance.should_receive(:dump).and_raise(Interrupt) subject.send(:generate_mysqldump, default_data_entry, default_sync_fm) }.to raise_error end end context 'with stream option' do it 'will export to io' do expect(default_sync_fm).to receive(:save_sync_info).once Flydata::Parser::Mysql::MysqlDumpGeneratorNoMasterData.any_instance.should_receive(:dump) subject.send(:generate_mysqldump, default_data_entry, default_sync_fm, false) end end end describe '#do_generate_table_ddl' do before do allow(subject).to receive(:data_entry).and_return(default_data_entry) allow_any_instance_of(Flydata::Api::DataEntry).to receive(:update_table_validity).and_return(true) subject.send(:set_current_tables, nil, include_all_tables: true) end shared_examples 'throws an error' do it "throws an error" do expect { subject.send(:do_generate_table_ddl, default_data_entry) }.to raise_error end end context 'with full options' do it 'issues mysqldump command with expected parameters' do expect(Open3).to receive(:popen3).with( 'mysqldump -h localhost -P 3306 -umasashi -pwelcome --default-character-set=utf8 --protocol=tcp -d sync_test table1 table2 table4 table3') subject.send(:do_generate_table_ddl, default_data_entry) end end context 'without_host' do before do default_data_entry['mysql_data_entry_preference'].delete('host') end include_examples 'throws an error' end context 'with empty host' do before do default_data_entry['mysql_data_entry_preference']['host'] = "" end include_examples 'throws an error' end context 'without_port' do before do default_data_entry['mysql_data_entry_preference'].delete('port') end it "uses the default port" do expect(Open3).to receive(:popen3).with( 'mysqldump -h localhost -umasashi -pwelcome --default-character-set=utf8 --protocol=tcp -d sync_test table1 table2 table4 table3') subject.send(:do_generate_table_ddl, default_data_entry) end end context 'with_port_override' do before do default_data_entry['mysql_data_entry_preference']['port'] = 1234 end it "uses the specified port" do expect(Open3).to receive(:popen3).with( 'mysqldump -h localhost -P 1234 -umasashi -pwelcome --default-character-set=utf8 --protocol=tcp -d sync_test table1 table2 table4 table3') subject.send(:do_generate_table_ddl, default_data_entry) end end context 'without_username' do before do default_data_entry['mysql_data_entry_preference'].delete('username') end include_examples 'throws an error' end context 'with empty username' do before do default_data_entry['mysql_data_entry_preference']['username'] = "" end include_examples 'throws an error' end context 'without_password' do before do default_data_entry['mysql_data_entry_preference'].delete('password') end it "call mysqldump without MYSQL_PW set" do expect(Open3).to receive(:popen3).with( 'mysqldump -h localhost -P 3306 -umasashi --default-character-set=utf8 --protocol=tcp -d sync_test table1 table2 table4 table3') subject.send(:do_generate_table_ddl, default_data_entry) end end context 'with password containing symbols' do before do default_data_entry['mysql_data_entry_preference'].delete('password') default_data_entry['mysql_data_entry_preference']['password']="welcome&!@^@#^" end it "call mysqldump with MYSQL_PW set with correct symbols" do expect(Open3).to receive(:popen3).with( 'mysqldump -h localhost -P 3306 -umasashi -pwelcome\\&\\!@\\^@\\#\\^ --default-character-set=utf8 --protocol=tcp -d sync_test table1 table2 table4 table3') subject.send(:do_generate_table_ddl, default_data_entry) end end context 'without_database' do before do default_data_entry['mysql_data_entry_preference'].delete('database') end include_examples 'throws an error' end context 'with empty database' do before do default_data_entry['mysql_data_entry_preference']['database'] = "" end include_examples 'throws an error' end context 'with empty tables' do let(:sync_cmd) { described_class.new } before do default_data_entry['mysql_data_entry_preference']['tables'] = [] default_data_entry['mysql_data_entry_preference']['invalid_tables'] = [] default_data_entry['mysql_data_entry_preference']['new_tables'] = [] allow(sync_cmd).to receive(:data_entry).and_return(default_data_entry) sync_cmd.send(:set_current_tables, nil, include_all_tables: true) end it 'should raise error' do expect{sync_cmd.send(:do_generate_table_ddl, default_data_entry)}.to raise_error end end end describe '#convert_to_flydata_values' do subject { subject_object.send(:convert_to_flydata_values, mysql_table, values) } let(:values) { [4, 'John', nil, col4_value, nil, nil] } before do mysql_table.set_value_converters(FlydataCore::TableDef::MysqlTableDef::VALUE_CONVERTERS) end context 'with binary column' do let(:col4_type) { "binary" } let(:col4_width) { 5 } let(:truncated_binary) { "0xC04482" } let(:col4_value) { "#{truncated_binary}0000" } it 'truncates trailing "0" if the type is binary' do expected_values = values.dup expected_values[3] = truncated_binary subject expect(values).to eq expected_values end end end end end end