1   /**
2    *  Copyright 2003-2007 Greg Luck
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.constructs.web;
18  
19  import com.meterware.httpunit.ClientProperties;
20  import com.meterware.httpunit.HttpUnitOptions;
21  import com.meterware.httpunit.WebConversation;
22  import com.meterware.httpunit.WebResponse;
23  import junit.framework.AssertionFailedError;
24  import junit.framework.TestCase;
25  import net.sf.ehcache.AbstractCacheTest;
26  import net.sf.ehcache.CacheManager;
27  import org.apache.commons.httpclient.HttpMethod;
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.dom4j.Document;
31  import org.dom4j.io.DOMReader;
32  import org.xml.sax.SAXException;
33  
34  import java.io.IOException;
35  import java.net.HttpURLConnection;
36  import java.util.List;
37  
38  /**
39   * A convenient base class for ehcache filter tests
40   *
41   * @author Greg Luck
42   * @version $Id: AbstractWebTest.java 512 2007-07-10 09:18:45Z gregluck $
43   */
44  public abstract class AbstractWebTest extends TestCase {
45  
46  
47      /**
48       * {@value}
49       */
50      public static final String CONTENT_TYPE = "CONTENT-TYPE";
51      /**
52       * {@value}
53       */
54      public static final String CONTENT_LENGTH = "CONTENT-LENGTH";
55      /**
56       * {@value}
57       */
58      public static final String CONTENT_ENCODING = "CONTENT-ENCODING";
59      /**
60       * {@value}
61       */
62      public static final String CONNECTION = "CONNECTION";
63      /**
64       * {@value}
65       */
66      public static final String SERVER = "SERVER";
67      /**
68       * {@value}
69       */
70      public static final String DATE = "DATE";
71      /**
72       * {@value}
73       */
74      public static final String KEEP_ALIVE = "KEEP-ALIVE";
75  
76      private static final Log LOG = LogFactory.getLog(AbstractWebTest.class.getName());
77  
78      /**
79       * Run web tests in a caching cluster. They use a singleton, so create a second
80       * instance CacheManager with the same config.
81       */
82      private CacheManager instanceManager;
83  
84      /**
85       * @throws Exception
86       */
87      protected void setUp() throws Exception {
88          instanceManager = new CacheManager(AbstractCacheTest.TEST_CONFIG_DIR + "ehcache.xml");
89      }
90  
91      /**
92       * @throws Exception
93       */
94      protected void tearDown() throws Exception {
95          instanceManager.shutdown();
96      }
97  
98  
99      /**
100      * Checks that the expected string occurs within the content string.
101      */
102     protected static void assertContains(final String string, final String content) {
103         if (content.indexOf(string) == -1) {
104             throw new AssertionFailedError(content + "' does not contain '" + string + "'");
105         }
106     }
107 
108     /**
109      * Performs an HTTP request, and returns the response.
110      * <p/>
111      * The request is set to accept gzip encoding. Note that HttpUnit automatically gunzips the response
112      * probived the "Content-encoding: gzip" response header is set.
113      */
114     protected WebResponse getResponseFromAcceptGzipRequest(final String uri) throws IOException, SAXException {
115         final WebConversation conversation = createWebConversation(true);
116         final WebResponse response = conversation.getResponse(buildUrl(uri));
117         return response;
118     }
119 
120     /**
121      * Performs an HTTP request, and returns the response.
122      */
123     protected WebResponse getResponseFromNonAcceptGzipRequest(final String uri) throws IOException, SAXException {
124         final WebConversation conversation = createWebConversation(false);
125         final WebResponse response = conversation.getResponse(buildUrl(uri));
126         return response;
127     }
128 
129     /**
130      * Creates a new WebConversation to use for this test.
131      */
132     protected WebConversation createWebConversation(boolean acceptGzip) {
133         HttpUnitOptions.setExceptionsThrownOnScriptError(false);
134         HttpUnitOptions.setCheckContentLength(true);
135         HttpUnitOptions.setScriptingEnabled(false);
136         ClientProperties.getDefaultProperties().setAcceptGzip(acceptGzip);
137         final WebConversation conversation = new WebConversation();
138         return conversation;
139     }
140 
141     /**
142      * Builds a URL from a URI
143      *
144      * @param uri
145      * @return
146      */
147     protected String buildUrl(String uri) {
148         return "http://localhost:9080" + uri;
149     }
150 
151     /**
152      * Checks:
153      * <ol>
154      * <li>The response code is OK i.e. 200
155      * <li>The headers are sane.
156      * <li>The page is not blank.
157      * </ol>
158      */
159     protected void assertResponseGood(WebResponse response, boolean fullPage) {
160         assertResponseOk(response);
161         if (fullPage) {
162             assertHeadersSane(response);
163         } else {
164             assertIncludeHeadersSane(response);
165         }
166         assertPageNotBlank(response);
167         assertPropertlyFormed(response);
168     }
169 
170     /**
171      * Checks the response code is OK i.e. 200
172      *
173      * @param response
174      */
175     protected void assertResponseOk(WebResponse response) {
176         assertEquals(HttpURLConnection.HTTP_OK, response.getResponseCode());
177     }
178 
179     /**
180      * Subtle problems can occur when headers disagree with the content.
181      * <p/>
182      * Check:
183      * <ol>
184      * <li>Content type is text/html
185      * <li>The content length set in the header matches the actual content length
186      * <li>If the header says the page is gzipped, it really is.
187      * </ol>
188      */
189     protected void assertHeadersSane(WebResponse response) {
190         //Content type is text/html with an optional character set
191         String contentType = response.getHeaderField(CONTENT_TYPE);
192         assertTrue(contentType.equals("text/html")
193                 || contentType.equals("text/html; charset=utf-8")
194                 || contentType.equals("text/html;charset=utf-8")
195                 || contentType.equals("text/html;charset=ISO-8859-1"));
196 
197         //The content length set in the header matches the actual content length
198         String headerContentLength = response.getHeaderField(CONTENT_LENGTH);
199         if (headerContentLength != null) {
200             assertEquals("Content length matches header content length",
201                     Integer.parseInt(headerContentLength), response.getContentLength());
202         }
203     }
204 
205     /**
206      * Subtle problems can occur when headers disagree with the content.
207      *
208      * @param response
209      */
210     protected void assertIncludeHeadersSane(WebResponse response) {
211         String contentType = response.getHeaderField(CONTENT_TYPE);
212 
213         assertTrue(contentType == null
214                 || contentType.startsWith("text/html")
215                 || contentType.startsWith("text/plain"));
216 
217         String contentLength = response.getHeaderField(CONTENT_LENGTH);
218         //assertTrue(contentLength == null || contentLength.equals("0"));
219         assertNull(response.getHeaderField(CONTENT_ENCODING));
220     }
221 
222     /**
223      * Checks the included time produced comment on two pages to see if they
224      * are the same. Being the same means that the pages were produced at exactly the same
225      * time hence the page is cached.
226      * <p/>
227      * Relies on the existence of <code><!-- Generated at <%= date %> --></code> in the requested
228      * page.
229      */
230     protected void checkTimeStamps(WebResponse webResponse1, WebResponse webResponse2,
231                                    boolean shouldTimestampsBeEqual) throws Exception {
232         LOG.debug("Should timestamps be equal: " + shouldTimestampsBeEqual);
233         String firstGeneratedTimestamp = getTimestamp(webResponse1);
234         LOG.debug("First time stamp: " + firstGeneratedTimestamp);
235         String secondGeneratedTimestamp = getTimestamp(webResponse2);
236         LOG.debug("Second time stamp: " + secondGeneratedTimestamp);
237 
238 
239         // Use assert equals because it provides more information if the assertion fails
240         if (shouldTimestampsBeEqual) {
241             assertEquals(firstGeneratedTimestamp, secondGeneratedTimestamp);
242         } else {
243             assertFalse(firstGeneratedTimestamp.equals(secondGeneratedTimestamp));
244         }
245     }
246 
247     /**
248      * @return the server side rendering time stamp. Relies on the existence of a
249      *         <br> <code>&lt;!-- Generated at <%= date %> --></code><br>
250      *         scriptlet in the page.
251      */
252     protected String getTimestamp(final WebResponse response) throws Exception {
253         String generationPrefix = "Generated at ";
254         int index = response.getText().indexOf(generationPrefix);
255         String timestamp = response.getText().substring(index, index + 36);
256         return timestamp;
257     }
258 
259     /**
260      * Assert that the page cache was used.
261      * The method for determining the page cache is being used is by looking at the first 'Generated at'
262      * comment that appears.
263      */
264     protected void assertResponseGoodAndCached(String path, boolean fullPage) throws Exception {
265         WebResponse firstResponse = getResponseFromAcceptGzipRequest(path);
266         assertResponseGood(firstResponse, fullPage);
267         WebResponse secondResponse = getResponseFromAcceptGzipRequest(path);
268         assertResponseGood(secondResponse, fullPage);
269         checkTimeStamps(firstResponse, secondResponse, true);
270     }
271 
272     /**
273      * Assert that the page cache was not used.
274      * The method for determining the page cache is being used is by looking at the first 'Generated at'
275      * comment that appears.
276      */
277     protected void assertResponseGoodAndNotCached(String path, boolean fullPage) throws Exception {
278         WebResponse firstResponse = getResponseFromAcceptGzipRequest(path);
279         assertResponseGood(firstResponse, fullPage);
280         WebResponse secondResponse = getResponseFromAcceptGzipRequest(path);
281         assertResponseGood(secondResponse, fullPage);
282         checkTimeStamps(firstResponse, secondResponse, false);
283     }
284 
285     /**
286      * Checks the message on the form.
287      */
288     protected void checkMessage(final WebResponse response, final String expected)
289             throws SAXException {
290         final Document document = createDocument(response);
291         final String message = document.valueOf("//*[@id='message']");
292         assertEquals(expected, message);
293     }
294 
295     /**
296      * Creates a dom4j Document that represents the HTML body of a response.  This allows xpath expressions to be
297      * used to extract details from the document.
298      */
299     protected Document createDocument(final WebResponse response) throws SAXException {
300         response.getDOM();
301         final Document doc = new DOMReader().read(response.getDOM());
302         return doc;
303     }
304 
305     /**
306      * Runs a set of threads, for a fixed amount of time.
307      */
308     protected void runThreads(final List executables) throws Exception {
309         final long endTime = System.currentTimeMillis() + 10000;
310         final Throwable[] errors = new Throwable[1];
311 
312         // Spin up the threads
313         final Thread[] threads = new Thread[executables.size()];
314         for (int i = 0; i < threads.length; i++) {
315             final AbstractWebTest.Executable executable = (AbstractWebTest.Executable) executables.get(i);
316             threads[i] = new Thread() {
317                 public void run() {
318                     try {
319                         // Run the thread until the given end time
320                         while (System.currentTimeMillis() < endTime) {
321                             executable.execute();
322                         }
323                     } catch (Throwable t) {
324                         // Hang on to any errors
325                         errors[0] = t;
326                     }
327                 }
328             };
329 
330             threads[i].start();
331         }
332         LOG.debug("Started " + threads.length + " threads");
333 
334         // Wait for the threads to finish
335         for (int i = 0; i < threads.length; i++) {
336             threads[i].join();
337         }
338 
339         // Throw any error that happened
340         if (errors[0] != null) {
341             throw new Exception("Test thread failed.", errors[0]);
342         }
343     }
344 
345 
346     /**
347      * A runnable, that can throw an exception.
348      */
349     protected interface Executable {
350 
351         /**
352          * Executes this object.
353          *
354          * @throws Exception
355          */
356         void execute() throws Exception;
357     }
358 
359 
360     /**
361      * Checks for the page is not blank.
362      * A blank page is a valid response but with a content of 0 bytes. It is a subtle error.
363      * This can happen in certain cirumstances when no-one writes to the OutputStream at all.
364      *
365      * @param response
366      */
367     protected void assertPageNotBlank(WebResponse response) {
368         String body = null;
369         try {
370             body = response.getText();
371         } catch (IOException e) {
372             LOG.fatal(e, e);
373             fail();
374         }
375         assertNotNull(body);
376         assertTrue(!body.equals(""));
377     }
378 
379     /**
380      * Checks the response can be parsed into an XML document. Will fail if not properly formed.
381      *
382      * @param response
383      */
384     protected void assertPropertlyFormed(WebResponse response) {
385 //        try {
386 //            createDocument(response);
387 //        } catch (SAXException e) {
388 //            LOG.fatal(e.getMessage(), e);
389 //            fail();
390 //        }
391         String text = null;
392         try {
393             text = response.getText();
394         } catch (IOException e) {
395             LOG.fatal(e.getMessage(), e);
396             fail();
397         }
398         assertTrue(text.indexOf("<html>") != -1);
399         assertTrue(text.indexOf("</html>") != -1);
400     }
401 
402     /**
403      * Orion returns a length of 0. Tomcat does not set content length at all.
404      *
405      * @param httpMethod
406      */
407     protected void checkNullOrZeroContentLength(HttpMethod httpMethod) {
408         boolean nullContentLengthHeader = httpMethod.getResponseHeader("Content-Length") == null;
409         if (!nullContentLengthHeader) {
410             assertEquals("0", httpMethod.getResponseHeader("Content-Length").getValue());
411         }
412     }
413 
414 }
415