package com.limegroup.gnutella.filters; import java.util.Collections; import java.util.concurrent.Callable; import junit.framework.Test; import org.jmock.Expectations; import org.jmock.Mockery; import org.jmock.api.Invocation; import org.jmock.lib.action.CustomAction; import org.limewire.io.GUID; import com.limegroup.gnutella.LimeTestUtils; import com.limegroup.gnutella.helpers.UrnHelper; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.messages.QueryRequestFactory; import com.limegroup.gnutella.util.LimeTestCase; /** * Unit tests for DuplicateFilter */ // TODO convert to BaseTestCase, get rid of LimeXMLDocument dependencies in last test case public class DuplicateFilterTest extends LimeTestCase { DuplicateFilter filter; PingRequest pr; QueryRequest qr; private Mockery context; private QueryRequestFactory queryRequestFactory; public DuplicateFilterTest(String name) { super(name); } public static Test suite() { return buildTestSuite(DuplicateFilterTest.class); } public static void main(String[] args) { junit.textui.TestRunner.run(suite()); } @Override protected void setUp() throws Exception { filter = new DuplicateFilter(); filter.setQueryLag(50); filter.setGUIDLag(50); context = new Mockery(); qr = context.mock(QueryRequest.class); pr = context.mock(PingRequest.class); queryRequestFactory = LimeTestUtils.createInjector().getInstance(QueryRequestFactory.class); } private void addDefaultReturnValues() { // specify default return values context.checking(new Expectations() {{ allowing(pr).getHops(); will(returnValue((byte)2)); allowing(qr).getHops(); will(returnValue((byte)2)); allowing(qr).getQuery(); will(returnValue("blah")); allowing(qr).getRichQuery(); will(returnValue(null)); allowing(qr).getQueryUrns(); will(returnValue(Collections.emptySet())); allowing(qr).getMetaMask(); will(returnValue(QueryRequest.AUDIO_MASK)); allowing(qr).getGUID(); will(new CallableAction(new Callable<byte[]>() { public byte[] call() throws Exception { return new GUID().bytes(); } })); }}); } public void testPingAndQueryWithSameGUIDAreRejected() throws Exception { final GUID guid = new GUID(); context.checking(new Expectations() {{ exactly(1).of(pr).getGUID(); will(returnValue(guid.bytes())); exactly(2).of(qr).getGUID(); will(returnValue(guid.bytes())); }}); addDefaultReturnValues(); assertTrue(filter.allow(pr)); assertFalse(filter.allow(qr)); waitForGUIDFilterToBePurged(); assertTrue(filter.allow(qr)); context.assertIsSatisfied(); } public void testSameGUIDPingIsNotAllowedBeforeTimeout() throws Exception { final GUID guid = new GUID(); context.checking(new Expectations() {{ exactly(3).of(pr).getGUID(); will(returnValue(guid.bytes())); }}); addDefaultReturnValues(); assertTrue(filter.allow(pr)); assertFalse(filter.allow(pr)); waitForGUIDFilterToBePurged(); assertTrue(filter.allow(pr)); context.assertIsSatisfied(); } public void testSameGUIDDifferentHopCountAllowed() { context.checking(new Expectations() {{ GUID guid = new GUID(); allowing(pr).getGUID(); will(returnValue(guid.bytes())); one(pr).getHops(); will(returnValue((byte)2)); one(pr).getHops(); will(returnValue((byte)3)); }}); addDefaultReturnValues(); assertTrue(filter.allow(pr)); assertTrue(filter.allow(pr)); context.assertIsSatisfied(); } // wait for guid filter to be purged private void waitForGUIDFilterToBePurged() throws Exception { synchronized (filter) { try { int lag = filter.getGUIDLag() * 2; assertGreaterThan(0, lag); filter.wait(lag); } catch (InterruptedException e) { } } } private void waitForQueryRequestFilterToBePurged() throws Exception { synchronized (filter) { try { int lag = filter.getQueryLag() * 3; assertGreaterThan(0, lag); filter.wait(lag); } catch (InterruptedException e) { } } } public void testQueryStringDuplicate() throws Exception { context.checking(new Expectations() {{ exactly(2).of(qr).getQuery(); will(returnValue("search1")); exactly(3).of(qr).getQuery(); will(returnValue("search2")); exactly(2).of(qr).getQuery(); will(returnValue("search3")); exactly(12).of(qr).getHops(); will(returnValue((byte)2)); exactly(2).of(qr).getHops(); will(returnValue((byte)3)); }}); addDefaultReturnValues(); assertTrue("pristine state, should be allowed", filter.allow(qr)); assertFalse("same query in there, not allowed", filter.allow(qr)); assertTrue("different query, should be allowed", filter.allow(qr)); waitForQueryRequestFilterToBePurged(); assertTrue("cache cleared, same query should be allowed", filter.allow(qr)); assertFalse("same query, not allowed", filter.allow(qr)); assertTrue("different query, allowed", filter.allow(qr)); assertTrue("same query, different hop, allowed", filter.allow(qr)); context.assertIsSatisfied(); } public void testURNDuplicate() throws Exception { context.checking(new Expectations() {{ exactly(2).of(qr).getQueryUrns(); will(returnValue(Collections.singleton(UrnHelper.SHA1))); exactly(3).of(qr).getQueryUrns(); will(returnValue(Collections.singleton(UrnHelper.UNIQUE_SHA1))); }}); addDefaultReturnValues(); assertTrue("pristine state, allowed", filter.allow(qr)); assertFalse("same urn query, not allowed", filter.allow(qr)); assertTrue("different urn query, allowed", filter.allow(qr)); assertFalse("same urn query, not allowed", filter.allow(qr)); waitForQueryRequestFilterToBePurged(); assertTrue("cache cleared, same urn query allowed", filter.allow(qr)); context.assertIsSatisfied(); } // TODO, remove dependencies by mocking LimeXMLDocument public void testXMLDuplicate() throws Exception { // use default values, construction takes longer for real query results filter.setQueryLag(DuplicateFilter.QUERY_LAG); filter.setGUIDLag(DuplicateFilter.GUID_LAG); // Only allowed once in the timeframe ... qr = queryRequestFactory.createQuery("tests"); assertTrue(filter.allow(qr)); assertFalse(filter.allow(qr)); // Invalid XML, considered same as plaintext. qr = queryRequestFactory.createQuery("tests", "<?xml"); assertFalse(filter.allow(qr)); qr = queryRequestFactory.createQuery("tests", "<?xml version=\"1.0\"?>" + "<audios xsi:noNamespaceSchemaLocation=" + "\"http://www.limewire.com/schemas/audio.xsd\">" + "<audio title=\"sam\" artist=\"sam's band\"></audio></audios>"); // same plain-text, different XML, allowed ... assertTrue(filter.allow(qr)); assertTrue(!filter.allow(qr)); qr = queryRequestFactory.createQuery("another test", "<?xml version=\"1.0\"?>" + "<audios xsi:noNamespaceSchemaLocation=" + "\"http://www.limewire.com/schemas/audio.xsd\">" + "<audio title=\"sam\" artist=\"sam's band\"></audio></audios>"); // same XML, different plaint-text, allowed ... assertTrue(filter.allow(qr)); assertTrue(!filter.allow(qr)); qr = queryRequestFactory.createQuery("another test", "<?xml version=\"1.0\"?>" + "<audios xsi:noNamespaceSchemaLocation=" + "\"http://www.limewire.com/schemas/audio.xsd\">" + "<audio title=\"sam\" artist=\"sam's choir\"></audio></audios>"); // different XML, allowed ... assertTrue(filter.allow(qr)); assertTrue(!filter.allow(qr)); qr = queryRequestFactory.createQuery("another test", "<?xml version=\"1.0\"?>" + "<audios xsi:noNamespaceSchemaLocation=" + "\"http://www.limewire.com/schemas/audio.xsd\">" + "<audio title=\"sam\" artist=\"sam's choir\"></audio></audios>"); //same XML and plain-text, not allowed. assertTrue(!filter.allow(qr)); } private static class CallableAction extends CustomAction { private Callable callable; public CallableAction(Callable callable) { super("Calls a callable"); this.callable = callable; } public Object invoke(Invocation invocation) throws Throwable { return callable.call(); } } }