require 'spec_helper' describe GetWork do let(:austin_geo) { austin_geoip } let(:dallas_geo) { dallas_geoip } before(:each) do create_phony_database end describe "get_work_list" do it "selects no score when no other clients" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) GetWork.get_work_list(my_connection).should == [] end it "selects unscored location" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) GetWork.get_work_list(my_connection).should == [other_connection.client_id] GetWork.get_work_list(other_connection).should == [my_connection.client_id] end it "skips scored location" do score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) GetWork.get_work_list(my_connection).should == [] GetWork.get_work_list(other_connection).should == [] end it "selects scored location with old scores" do score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) # age the scores Score.connection.execute("UPDATE scores SET score_dt = score_dt - INTERVAL '200 hours'") my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) GetWork.get_work_list(my_connection).should == [other_connection.client_id] GetWork.get_work_list(other_connection).should == [my_connection.client_id] end it "skips scored location with old and new scores" do score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) # age the scores Score.connection.execute("UPDATE scores SET score_dt = score_dt - INTERVAL '200 hours'") # create some newer ones score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) GetWork.get_work_list(my_connection).should == [] GetWork.get_work_list(other_connection).should == [] end it "skips scores regardess of scoring direction" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) score_location(dallas_geo[:locidispid], austin_geo[:locidispid], 20) GetWork.get_work_list(my_connection).should == [] GetWork.get_work_list(other_connection).should == [] Score.connection.execute('DELETE from scores').check score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) GetWork.get_work_list(my_connection).should == [] GetWork.get_work_list(other_connection).should == [] end it "selects client even if client has scores to self" do # this test just verifies that a bit of data in the db doesn't trip up the query my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) score_location(dallas_geo[:locidispid], dallas_geo[:locidispid], 20) GetWork.get_work_list(my_connection).should == [other_connection.client_id] GetWork.get_work_list(other_connection).should == [my_connection.client_id] end it "selects only one client from a given remote location" do # if two clients have the same locidispid, only one is meant to be selected for scoring my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection1 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) other_connection2 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 3) list = GetWork.get_work_list(my_connection) (list == [other_connection1.client_id] || list == [other_connection2.client_id]).should be_true # we don't know which one it'll pick GetWork.get_work_list(other_connection1).should =~ [my_connection.client_id, other_connection2.client_id] GetWork.get_work_list(other_connection2).should =~ [my_connection.client_id, other_connection1.client_id] end it "selects no clients when multiple clients in same location have a score" do # if two clients have the same locidispid, only one is meant to be selected for scoring my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection1 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) other_connection2 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 3) score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) GetWork.get_work_list(my_connection).should == [] GetWork.get_work_list(other_connection1).should == [other_connection2.client_id] GetWork.get_work_list(other_connection2).should == [other_connection1.client_id] end it "selects two clients from differing, unscored locations" do # if two clients have the same locidispid, only one is meant to be selected for scoring my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection1 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) GetWork.get_work_list(my_connection).should =~ [other_connection1.client_id, other_connection2.client_id] GetWork.get_work_list(other_connection1).should =~ [my_connection.client_id, other_connection2.client_id] GetWork.get_work_list(other_connection2).should =~ [my_connection.client_id, other_connection1.client_id] end it "ignores client with the same addr" do # if two clients have the same locidispid, only one is meant to be selected for scoring my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection1 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 1) GetWork.get_work_list(my_connection).should == [] end it "ignores client with the same addr" do # if two clients have the same locidispid, only one is meant to be selected for scoring my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection1 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 1) GetWork.get_work_list(my_connection).should == [] end it "randomizes ordering of selected locations" do # if two clients have the same locidispid, only one is meant to be selected for scoring my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection1 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) initial_ordering = GetWork.get_work_list(my_connection) initial_ordering.should =~ [other_connection1.client_id, other_connection2.client_id] swapped = false 100.times do # it's randomized results, so we have to let probability win out here. eventually, though (within 100 times? surely), we should see the ordering of work switch up swapped = (GetWork.get_work_list(my_connection) == [initial_ordering[1], initial_ordering[0]]) break if swapped end swapped.should be_true end it "excludes udp unreachable clients" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2, udp_reachable: false) other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) GetWork.get_work_list(my_connection).should == [other_connection2.client_id] GetWork.get_work_list(other_connection).should == [] GetWork.get_work_list(other_connection2).should == [my_connection.client_id] end it "excludes network testing clients" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2, is_network_testing: true) other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) GetWork.get_work_list(my_connection).should == [other_connection2.client_id] GetWork.get_work_list(other_connection).should == [] GetWork.get_work_list(other_connection2).should == [my_connection.client_id] end it "excludes scoring_timeout clients (1)" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2, scoring_timeout: 1.days.from_now) other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) GetWork.get_work_list(my_connection).should == [other_connection2.client_id] GetWork.get_work_list(other_connection).should == [] GetWork.get_work_list(other_connection2).should == [my_connection.client_id] end it "excludes scoring_timeout clients (2)" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2, scoring_timeout: 1.days.from_now) other_connection2 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 3) GetWork.get_work_list(my_connection).should == [other_connection2.client_id] GetWork.get_work_list(other_connection).should == [] GetWork.get_work_list(other_connection2).should == [my_connection.client_id] end it "excludes connections in a session" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2,) other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) music_session = FactoryGirl.create(:active_music_session, creator: my_connection.user) other_connection.music_session = music_session other_connection.save! GetWork.get_work_list(my_connection).should == [other_connection2.client_id] GetWork.get_work_list(other_connection).should == [] GetWork.get_work_list(other_connection2).should == [my_connection.client_id] end end describe "record" do let(:connection1) { FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: austin_ip_as_num, ip_address: austin_ip) } let(:connection2) { FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: dallas_ip_as_num, ip_address: dallas_ip) } it "records client errors if no score" do original_timeout1 = connection1.scoring_timeout original_timeout2 = connection2.scoring_timeout result = Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, nil, '', false) result.should == {message: 'udpReachable is false (to=f)', error: true} connection1.reload connection2.reload connection1.scoring_failures.should == 1 expect(connection1.scoring_timeout).to be_within(1.second).of(original_timeout1) connection1.scoring_timeout_occurrences.should == 0 connection2.scoring_failures.should == 1 expect(connection2.scoring_timeout).to be_within(1.second).of(original_timeout2) connection1.scoring_timeout_occurrences.should == 0 end it "records client errors if addr == addr" do original_timeout1 = connection1.scoring_timeout original_timeout2 = connection2.scoring_timeout result = Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection1.ip_address, nil, '', false) result.should == {message: 'aAddr and bAddr are the same (to=f)', error: true} connection1.reload connection2.reload connection1.scoring_failures.should == 1 expect(connection1.scoring_timeout).to be_within(1.second).of(original_timeout1) connection1.scoring_timeout_occurrences.should == 0 connection2.scoring_failures.should == 1 expect(connection2.scoring_timeout).to be_within(1.second).of(original_timeout2) connection1.scoring_timeout_occurrences.should == 0 end it "records success if valid" do original_timeout1 = connection1.scoring_timeout original_timeout2 = connection2.scoring_timeout result = Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, 20, '', true) result.should == {message: 'OK (to=f)', error: false} connection1.reload connection2.reload connection1.scoring_failures.should == 0 expect(connection1.scoring_timeout).to be_within(1.second).of(original_timeout1) connection1.scoring_timeout_occurrences.should == 0 connection2.scoring_failures.should == 0 expect(connection2.scoring_timeout).to be_within(1.second).of(original_timeout2) connection1.scoring_timeout_occurrences.should == 0 end it "puts in doghouse after enough scoring errors" do last_result = nil APP_CONFIG.scoring_timeout_threshold.times do last_result = Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, nil, '', false) end last_result.should == {message: 'udpReachable is false (to=t)', error: true} connection1.reload connection2.reload connection1.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold connection1.scoring_failures_offset.should == APP_CONFIG.scoring_timeout_threshold expect(connection1.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection1.scoring_timeout_occurrences.should == 1 connection2.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold connection2.scoring_failures_offset.should == APP_CONFIG.scoring_timeout_threshold expect(connection2.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection2.scoring_timeout_occurrences.should == 1 end describe "while in the doghouse" do before(:each) do APP_CONFIG.scoring_timeout_threshold.times do Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, nil, '', false) end end it "another bad score comes in" do Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, nil, '', false) connection1.reload connection2.reload connection1.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold + 1 expect(connection1.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection1.scoring_timeout_occurrences.should == 1 connection2.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold + 1 expect(connection2.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection2.scoring_timeout_occurrences.should == 1 end it "a good score comes in" do # this has no effect when in the dog house Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, 20, '', true) connection1.reload connection2.reload connection1.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold expect(connection1.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection1.scoring_timeout_occurrences.should == 1 connection2.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold expect(connection2.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection2.scoring_timeout_occurrences.should == 1 end end describe "after doghouse expires" do before(:each) do APP_CONFIG.scoring_timeout_threshold.times do Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, nil, '', false) end # bring scoring_timeout to the past connection1.scoring_timeout = (APP_CONFIG.scoring_timeout_minutes * 2).minutes.ago connection1.save! connection2.scoring_timeout = (APP_CONFIG.scoring_timeout_minutes * 2).minutes.ago connection2.save! end it "another bad score comes in" do original_timeout1 = connection1.scoring_timeout original_timeout2 = connection2.scoring_timeout Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, nil, '', false) connection1.reload connection2.reload # failures shold keep increment, but the user should not yet in_scoring_timeout? because it's only one failure connection1.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold + 1 connection1.in_scoring_timeout?.should be_false expect(connection1.scoring_timeout).to be_within(1.second).of(original_timeout1) connection1.scoring_timeout_occurrences.should == 1 connection2.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold + 1 connection2.in_scoring_timeout?.should be_false expect(connection2.scoring_timeout).to be_within(1.second).of(original_timeout2) connection2.scoring_timeout_occurrences.should == 1 end it "a good score comes in" do original_timeout1 = connection1.scoring_timeout original_timeout2 = connection2.scoring_timeout # this has no effect when in the dog house Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, 20, '', true) connection1.reload connection2.reload connection1.scoring_failures.should == 0 connection1.in_scoring_timeout?.should be_false expect(connection1.scoring_timeout).to be_within(1.second).of(original_timeout1) connection1.scoring_timeout_occurrences.should == 1 connection2.scoring_failures.should == 0 connection2.in_scoring_timeout?.should be_false expect(connection2.scoring_timeout).to be_within(1.second).of(original_timeout2) connection2.scoring_timeout_occurrences.should == 1 end it "a good score comes in, then enough bad scores to be put back into timeout" do Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, 20, '', true) # if a good score comes in while in the dog house, everything should be set back to 0, and bad counting resumes APP_CONFIG.scoring_timeout_threshold.times do Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, nil, '', false) end connection1.reload connection2.reload connection1.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold connection1.scoring_failures_offset.should == APP_CONFIG.scoring_timeout_threshold connection1.in_scoring_timeout?.should be_true expect(connection1.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection1.scoring_timeout_occurrences.should == 2 connection2.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold connection2.scoring_failures_offset.should == APP_CONFIG.scoring_timeout_threshold connection2.in_scoring_timeout?.should be_true expect(connection2.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection2.scoring_timeout_occurrences.should == 2 end it "enough bad scores come in to put back into timeout" do APP_CONFIG.scoring_timeout_threshold.times do Score.record(connection1.user, connection1.client_id, connection1.ip_address, connection2.client_id, connection2.ip_address, nil, '', false) end connection1.reload connection2.reload connection1.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold * 2 # because this user keeps failing with no good scores connection1.scoring_failures_offset.should == APP_CONFIG.scoring_timeout_threshold * 2 # because this user keeps failing with no good scores connection1.in_scoring_timeout?.should be_true expect(connection1.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection1.scoring_timeout_occurrences.should == 2 connection2.scoring_failures.should == APP_CONFIG.scoring_timeout_threshold * 2 # because this user keeps failing with no good scores connection2.scoring_failures_offset.should == APP_CONFIG.scoring_timeout_threshold * 2 # because this user keeps failing with no good scores connection2.in_scoring_timeout?.should be_true expect(connection2.scoring_timeout).to be_within(1.second).of(APP_CONFIG.scoring_timeout_minutes.minutes.from_now) connection2.scoring_timeout_occurrences.should == 2 end end end describe "summary" do it "selects no score when no other clients" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) summary = GetWork.summary summary.length.should == 1 summary[0].work_count.should == 0 summary[0].client_id.should == my_connection.client_id summary[0].email.should == my_connection.user.email summary[0].user_id.should == my_connection.user.id end it "selects no score when no other clients" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1, udp_reachable:true) score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) summary = GetWork.summary summary.length.should == 1 summary[0].work_count.should == 0 summary[0].client_id.should == my_connection.client_id summary[0].email.should == my_connection.user.email summary[0].user_id.should == my_connection.user.id end it "selects unscored location" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) #score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) summary = GetWork.summary summary.length.should == 2 summary[0].work_count.should == 1 summary[1].work_count.should == 1 end it "does not count scored location" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) summary = GetWork.summary summary.length.should == 2 summary[0].work_count.should == 0 summary[1].work_count.should == 0 end it "does not count duplicate location" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) other_connection2 = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) score_location(austin_geo[:locidispid], dallas_geo[:locidispid], 20) summary = GetWork.summary summary.length.should == 3 summary[0].work_count.should == 0 summary[1].work_count.should == 0 summary[2].work_count.should == 0 end it "does not count udp_reachable" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2, udp_reachable: false) summary = GetWork.summary summary.length.should == 2 summary[0].work_count.should == 0 summary[1].work_count.should == 0 end it "does not count udp_reachable with 2 other clients" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2, udp_reachable: false) other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) summary = GetWork.summary summary.length.should == 3 summary[0].work_count.should == 1 summary[1].work_count.should == 1 summary[2].work_count.should == 0 end it "counts with 3" do my_connection = FactoryGirl.create(:connection, locidispid: austin_geo[:locidispid], addr: 1) other_connection = FactoryGirl.create(:connection, locidispid: dallas_geo[:locidispid], addr: 2) other_connection2 = FactoryGirl.create(:connection, locidispid: houston_geoip[:locidispid], addr: 3) #Database.dump('select * FROM get_work_summary_no_agg(INTERVAL \'120 hours\')'); summary = GetWork.summary summary.length.should == 3 summary[0].work_count.should == 2 summary[1].work_count.should == 2 summary[2].work_count.should == 2 end end end