001    /*
002    Copyright (c) 1996-2012, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.test.dev;
031    
032    import java.io.ByteArrayInputStream;
033    import java.io.DataInputStream;
034    import java.io.IOException;
035    import java.net.HttpURLConnection;
036    import java.net.URL;
037    import java.net.URLConnection;
038    import java.util.Random;
039    
040    import javax.servlet.ServletContext;
041    
042    import junit.framework.TestCase;
043    
044    import org.hd.d.pg2k.svrCore.AbstractSimpleLogger;
045    import org.hd.d.pg2k.svrCore.AllExhibitImmutableData;
046    import org.hd.d.pg2k.svrCore.AllExhibitProperties;
047    import org.hd.d.pg2k.svrCore.CoreConsts;
048    import org.hd.d.pg2k.svrCore.Name;
049    import org.hd.d.pg2k.svrCore.PGMasterNotInServiceException;
050    import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
051    import org.hd.d.pg2k.svrCore.datasource.ExhibitDataHTTPTunnelSource;
052    import org.hd.d.pg2k.svrCore.datasource.ExhibitDataTunnelSource;
053    import org.hd.d.pg2k.svrCore.datasource.ExhibitDataTunnelSource.RawPacket;
054    import org.hd.d.pg2k.svrCore.datasource.ExhibitDataTunnelSource.RawPacket.OpCode;
055    import org.hd.d.pg2k.svrCore.datasource.SimpleExhibitPipelineIF;
056    import org.hd.d.pg2k.svrCore.props.GenProps;
057    import org.hd.d.pg2k.svrCore.vars.SimpleVariableValue;
058    import org.hd.d.pg2k.svrCore.vars.SystemVariables;
059    import org.hd.d.pg2k.webSvr.exhibit.DataSourceBean;
060    
061    /**Test the HTTP communications tunnel.
062     * May need to be running in a WAR container and in master mode
063     * for this to work
064     * (unless, exceptionally, the loopbackURL is pointing at another instance).
065     */
066    public final class HTTPTunnelTest extends TestCase
067        {
068        public HTTPTunnelTest(final String name)
069            {
070            super(name);
071            }
072    
073        private final SimpleLoggerIF logger = new AbstractSimpleLogger(){
074            public void log(final String message) { Main.getOut().println(message); }
075            };
076    
077        /**Test that our hand-crafted serialisation routines work.
078         * This primarily means that we can decode with DataInputStream
079         * what we hand-encoded.
080         */
081        public static void testIntSerRoutines()
082            throws Exception
083            {
084            // Test for a few values...
085            for(int i = 10; --i >= 0; )
086                {
087                final int i1 = rnd.nextInt();
088                final DataInputStream dis1 = new DataInputStream(
089                    new ByteArrayInputStream(
090                        ExhibitDataTunnelSource.intSer(i1)));
091                assertEquals("DataInputStream.readInt() must be able to decode output of intSer()",
092                    i1, dis1.readInt());
093    
094                final long l1 = rnd.nextLong();
095                final DataInputStream dis2 = new DataInputStream(
096                    new ByteArrayInputStream(
097                        ExhibitDataTunnelSource.longSer(l1)));
098                assertEquals("DataInputStream.readInt() must be able to decode output of longSer()",
099                    l1, dis2.readLong());
100                }
101            }
102    
103        /**Test that stream-serialising compression works... */
104        public static void testRawPacketStreamSerialiseObject()
105            throws Exception
106            {
107            // Null object to serialise.
108            final RawPacket rp0 = RawPacket.streamSerialiseObject(OpCode.NOOP, null);
109            assertNull("Must be able to stream-serialise null object", rp0.getSerializedObjectPayload());
110    
111            // Random small serialisable payload.
112            final Long l1 = rnd.nextLong();
113            // Construct packet...
114            final RawPacket rp1 = RawPacket.streamSerialiseObject(OpCode.NOOP, l1);
115            // Deserialise from packet.
116            final Object o1 = rp1.getSerializedObjectPayload();
117            assertNotNull(o1);
118            assertEquals(l1, o1);
119            assertFalse("expected a new instance from deserialisation", l1 == o1);
120    
121            // Random small serialisable payload.
122            final Name n2 = Name.create("The quick brown fox jumps over the lazy dog: " + rnd.nextLong());
123            // Construct packet...
124            final RawPacket rp2 = RawPacket.streamSerialiseObject(OpCode.NOOP, n2);
125            // Deserialise from packet.
126            final Object o2 = rp2.getSerializedObjectPayload();
127            assertNotNull(o2);
128            assertEquals(n2, o2);
129            }
130    
131        /**Check that we can open a basic HTTP connection OK to the loopbackURL.
132         * Returns immediately if no loopbackURL is set.
133         */
134        public static boolean canConnectToLoopbackURL()
135            {
136            final URL lb = Main.getLoopbackURL();
137            if(lb == null) { return(false); }
138            try
139                {
140                final URLConnection conn = lb.openConnection();
141                conn.setUseCaches(false); // Force new connection each time.
142                if(conn instanceof HttpURLConnection)
143                    {
144                    final HttpURLConnection hconn = (HttpURLConnection) conn;
145                    hconn.setInstanceFollowRedirects(true); // Allow redirects.
146    
147                    // Check that the response is a 200 code.
148                    final int respCode = hconn.getResponseCode();
149                    if(respCode == HttpURLConnection.HTTP_OK)
150                        { return(true); } // HTTP "OK" means loopback is OK.
151                    }
152                }
153            catch(final IOException e)
154                { return(false); } // Regard this as a failure.
155    
156            // Assume failure if we get here.
157            return(false);
158            }
159    
160        /**If ServletContext exists and we are in master mode, return DataSource.
161         * Also, we check that basic loop-back is OK.
162         * <p>
163         * Else we return null.
164         */
165        public static DataSourceBean getDataSourceBeanIfMaster()
166            {
167            if(!canConnectToLoopbackURL()) { return(null); }
168            final ServletContext sc = (ServletContext) Main.getServletContext();
169            if(sc == null) { return(null); }
170            final DataSourceBean dsb = DataSourceBean.getApplicationInstance(sc);
171            if(!dsb.isSlave().equals(Boolean.FALSE)) { return(null); }
172            return(dsb);
173            }
174    
175        /**Does basic loopback test.
176         * If not, does not cause a failure but warns that this is not possible.
177         * Mainly useful in case other tests are silently disabled by this.
178         */
179        public static void testHTTPLoopback()
180            {
181            if(!canConnectToLoopbackURL())
182                {
183                Main.getErr().println("WARNING: HTTPTunnelTest: cannot connect to loopback HTTP[S] server, some tests may be disabled: " +
184                                      Main.getLoopbackURL());
185                }
186    
187            final DataSourceBean dsb = getDataSourceBeanIfMaster();
188            if(dsb == null)
189                {
190                Main.getErr().println("WARNING: HTTPTunnelTest: no servlet context, test skipped");
191                return;
192                }
193            }
194    
195        /**Attempts basic test of tunnel by doing a NO-OP (NOOP).
196         * May require us to be in "master" mode.
197         */
198        public void testHTTPTunnelNOOP()
199            throws Exception
200            {
201            final DataSourceBean dsb = getDataSourceBeanIfMaster();
202            if(dsb == null)
203                {
204                Main.getErr().println("WARNING: HTTPTunnelTest: no servlet context, test skipped");
205                return;
206                }
207    
208            final ServletContext ctxt = (ServletContext) Main.getServletContext();
209            // Get the usual URL for connecting to the master server tunnel...
210            final URL masterURL = new URL(ctxt.getInitParameter(CoreConsts.WAR_CTXTPARAM_BOOTURL));
211            // Then take the loopback URL and add the server tunnel URI...
212    
213            final String tunnelLoopbackURL = Main.getLoopbackURL() +
214                masterURL.getPath();
215    
216            // Now try to set up a tunnel connection.
217            final ExhibitDataTunnelSource ts =
218                new ExhibitDataHTTPTunnelSource(tunnelLoopbackURL, "test", logger);
219    
220            // Check that we can successfully send a NOOP request to the server.
221            // (Throws an IOException if not.)
222            ts.doNOOP(rnd.nextBoolean());
223            }
224    
225        /**Attempts to test basic use of the tunnel for normal traffic.
226         * May require us to be in "master" mode.
227         */
228        public void testTunnelBasic()
229            throws Exception
230            {
231            final DataSourceBean dsb = getDataSourceBeanIfMaster();
232            if(dsb == null)
233                {
234                Main.getErr().println("WARNING: HTTPTunnelTest: no servlet context, test skipped");
235                return;
236                }
237    
238            final ServletContext ctxt = (ServletContext) Main.getServletContext();
239    
240            // Get the usual URL for connecting to the master server tunnel...
241            final URL masterURL = new URL(ctxt.getInitParameter(CoreConsts.WAR_CTXTPARAM_BOOTURL));
242    
243            // Then take the loopback URL and add the server tunnel URI...
244            final String tunnelLoopbackURL = Main.getLoopbackURL() +
245                masterURL.getPath();
246    
247            // Now try to set up a tunnel connection.
248            final ExhibitDataTunnelSource ts =
249                new ExhibitDataHTTPTunnelSource(tunnelLoopbackURL, "test", logger);
250    
251            // Check that we can successfully send a NOOP request to the server.
252            // (Throws an IOException if not.)
253            ts.doNOOP(rnd.nextBoolean());
254    
255            // Now try to get the entire set of exhibit properties.
256            final AllExhibitProperties aepRemote = ts.getAllExhibitProperties(-1);
257            final AllExhibitProperties aep = dsb.getAllExhibitProperties(-1);
258            // Must not be null.
259            assertNotNull("AEP retrieved over tunnel must not be null", aepRemote);
260            // Must be equal to the set I have locally.
261            assertEquals("AEP retrieved over tunnel must match local version",
262                         aep, aepRemote);
263            // But must not be the same object,
264            // it it must really have been sent over the "wire".
265            assertNotSame("AEP retrieved over tunnel must have been (de)serialised",
266                          aep, aepRemote);
267    
268            // Now try to get the entire set of exhibit immutable properties.
269            final AllExhibitImmutableData aeidRemote = ts.getAllExhibitImmutableData(-1);
270            final AllExhibitImmutableData aeid = dsb.getAllExhibitImmutableData(-1);
271            // Must not be null.
272            assertNotNull("AEID retrieved over tunnel must not be null", aeidRemote);
273            // Must be equal to the set I have locally.
274            assertEquals("AEID retrieved over tunnel must match local version",
275                         aeid, aeidRemote);
276            // But must not be the same object,
277            // it it must really have been sent over the "wire".
278            assertNotSame("AEID retrieved over tunnel must have been (de)serialised",
279                          aeid, aeidRemote);
280    
281            // Now try to get the entire set of exhibit properties.
282            final GenProps gpRemote = ts.getGenProps(-1);
283            final GenProps gp = dsb.getGenProps(-1);
284            // Must not be null.
285            assertNotNull("GP retrieved over tunnel must not be null", gpRemote);
286            // NOTE: cannot test for equality because of local overrides of gp.
287            // Must not be the same object,
288            // it it must really have been sent over the "wire".
289            assertNotSame("GP retrieved over tunnel must have been (de)serialised",
290                          gp, gpRemote);
291    
292            // Now try to get the system ID.
293            // It should be different via the tunnel.
294            final SimpleVariableValue id1 = dsb.getVariable(SystemVariables.LOCAL_SYS_ID);
295            final SimpleVariableValue id2 = ts.getVariable(SystemVariables.LOCAL_SYS_ID);
296            assertNotNull("Should be able to retrieve system ID from DataSourceBean", id1);
297            assertNotNull("System ID from DataSourceBean should have non-null value", id1.getValue());
298            assertNotNull("Should be able to retrieve system ID from tunnel", id2);
299            assertNotNull("System ID from tunnel should have non-null value", id2.getValue());
300            assertFalse("System ID from DataSourceBean and tunnel should be different",
301                id1.getValue().equals(id2.getValue()));
302    
303    
304            // Test that we can set a global on either side of the tunnel
305            // and read it immediately on either side.
306            // We can't test for it being initially unset
307            // since the server may hold a value from a previous run.
308    //        assertNull("Must see null for unset global on master",
309    //            dsb.getVariable(SystemVariables.TEST_NUMBER_GLOBAL));
310    //        assertNull("Must see null for unset global on slave",
311    //            ts.getVariable(SystemVariables.TEST_NUMBER_GLOBAL));
312            // Set from global side, test on both.
313            final Number v1 = new Integer(1);
314            final SimpleVariableValue svv1 = new SimpleVariableValue(
315                SystemVariables.TEST_NUMBER_GLOBAL, v1);
316            dsb.setVariable(svv1);
317            assertEquals("Must be able to see on master variable set on master",
318                v1, dsb.getVariable(SystemVariables.TEST_NUMBER_GLOBAL).getValue());
319            assertEquals("Must be able to see on slave variable set on master",
320                v1, ts.getVariable(SystemVariables.TEST_NUMBER_GLOBAL).getValue());
321            // Now check that getVariables() is working on both sides...
322            assertTrue("Must be able to retrieve value set on master with getVariables(-1) on master",
323                SystemVariablesTest.checkValuePresent(dsb.getVariables(-1), svv1));
324            assertTrue("Must be able to retrieve value set on master with getVariables(-1) on slave",
325                SystemVariablesTest.checkValuePresent(ts.getVariables(-1), svv1));
326            // Set from slave side, test on both.
327            final Number v2 = new Short((short) 42);
328            final SimpleVariableValue svv2 = new SimpleVariableValue(
329                SystemVariables.TEST_NUMBER_GLOBAL, v2);
330            ts.setVariable(svv2);
331            assertEquals("Must be able to see on master variable set on slave",
332                v2, dsb.getVariable(SystemVariables.TEST_NUMBER_GLOBAL).getValue());
333            assertEquals("Must be able to see on slave variable set on slave",
334                v2, ts.getVariable(SystemVariables.TEST_NUMBER_GLOBAL).getValue());
335            // Now check that getVariables() is working on both sides...
336            assertTrue("Must be able to retrieve value set on slave with getVariables(-1) on master",
337                SystemVariablesTest.checkValuePresent(dsb.getVariables(-1), svv2));
338            assertTrue("Must be able to retrieve value set on slave with getVariables(-1) on slave",
339                SystemVariablesTest.checkValuePresent(ts.getVariables(-1), svv2));
340    
341            // Do general pipeline and tunnel tests...
342            SystemVariablesTest.pipelineVariableTest(
343                new SimpleExhibitPipelineIF[]{ts}, // Simple tunnel pipeline.
344                null,
345                new SimpleExhibitPipelineIF[]{dsb}, // Top of master pipeline.
346                System.currentTimeMillis() + 10000L); // Run for up to 10s.
347    
348            // Check that data is seen consistently on either side of the tunnel.
349            TunnelTest.checkExhibitDataConsistency(ts, dsb,
350                                                   true, // On two sides of a tunnel.
351                System.currentTimeMillis() + 10000L);  // Run for up to 10s.
352            }
353    
354        /**Test behaviour against dead master.
355         * We do this by connecting to a bogus loopback address.
356         * <p>
357         * We do not have to be master for this,
358         * nor even running as a servlet...
359         * <p>
360         * We simply check that we recover from a first dead connection,
361         * then subsequent connection attempts are refused for at least
362         * the minimum failure time and not much more than the maximum...
363         * These refusals should be with a "PGMasterNotInService" exception.
364         */
365        public void testAgainstDeadMaster()
366            throws Exception
367            {
368            Main.getOut().println("Checking for correct connection back-off against failed master...");
369    
370            // Bogus tunnel URL that should fail quickly (and locally/cheaply)...
371            final String bogusTunnelURL = "http://localhost:12345/bogus.noway.Jose";
372    
373            // Now try to set up a tunnel connection.
374            final ExhibitDataTunnelSource ts =
375                new ExhibitDataHTTPTunnelSource(bogusTunnelURL, "", logger);
376    
377            // Check that we cannot successfully send a NOOP request to the server.
378            // If this works, something is wrong!
379            try
380                {
381                ts.doNOOP(rnd.nextBoolean());
382                fail("This connection should not have succeeded!");
383                }
384            catch(final IOException e) /* We expect this to fail... */ { }
385    
386            final SimpleVariableValue sysID = ts.getVariable(SystemVariables.LOCAL_SYS_ID);
387            assertNotNull("Should be able to get system ID even with master down with getVariable()",
388                sysID);
389            // Just make sure that at least one value is returned...
390            final SimpleVariableValue[] variables = ts.getVariables(-1);
391            assertTrue("Should be able to get system ID even with master down with getVariables()",
392                SystemVariablesTest.checkValuePresent(variables, sysID));
393    
394    
395            // Now all subsequent attempts should be deferred by a time
396            // approximately bounded by the min/max retry times.
397            // We test this out a few times...
398            for(int i = 3; --i >= 0; )
399                {
400                final long start = System.currentTimeMillis();
401    
402                // Intensively try to talk to the master...
403                // We care about when we get the first "real" IOException
404                // indicating that a real connection attempt was made.
405                for( ; ; )
406                    {
407                    // Sleep a fairly short time before trying again...
408                    try { Thread.sleep(1 + rnd.nextInt(131)); }
409                    catch(final InterruptedException e) {}
410    
411                    try
412                        {
413                        // Should not be able to do NO-OP with master unreachable.
414                        ts.doNOOP(false); // This should show normal deferral behaviour...
415                        fail("This connection should not have succeeded!");
416                        }
417                    catch(final PGMasterNotInServiceException e) /* Good, deferring... */ { }
418                    catch(final IOException e)
419                        {
420                        // We take an IOException to indicate that real I/O was attempted again...
421                        final long realIOTime = System.currentTimeMillis();
422    
423                        // We compute how long "real" I/O was deferred...
424                        final long deferralTime = realIOTime - start;
425    
426                        assertTrue("We must wait at least about the minimum retry time before real I/O",
427                            deferralTime > ExhibitDataTunnelSource.FAIL_RETRY_WAIT_MIN_MS / 2);
428                        assertTrue("We must wait at most about the maximum retry time before real I/O",
429                            deferralTime < ExhibitDataTunnelSource.FAIL_RETRY_WAIT_MAX_MS * 2);
430    
431                        // OK, timing was reasonable.
432                        break;
433                        }
434                    }
435                }
436    
437            // Check that tunnel still behaves rationally with a dead master.
438            // Note that it is not required to be able to hold any values,
439            // though it must be able to return the system ID...
440            SystemVariablesTest.pipelineVariableTest(
441                new SimpleExhibitPipelineIF[]{ts}, null, null,
442                System.currentTimeMillis() + 10000L);
443            }
444    
445        /**Private source of OK pseudo-random numbers. */
446        private static final Random rnd = new Random();
447        }