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 /*
031 * Created by IntelliJ IDEA.
032 * User: Administrator
033 * Date: 28-Dec-02
034 * Time: 22:24:51
035 */
036 package org.hd.d.pg2k.test.dev;
037
038 import java.text.SimpleDateFormat;
039 import java.util.Arrays;
040 import java.util.Collections;
041 import java.util.Date;
042 import java.util.HashMap;
043 import java.util.List;
044 import java.util.Locale;
045 import java.util.Map;
046 import java.util.Set;
047 import java.util.Vector;
048
049 import javax.servlet.http.HttpServletRequest;
050 import javax.servlet.http.HttpServletResponse;
051
052 import junit.framework.TestCase;
053
054 import org.apache.http.message.BasicHeader;
055 import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
056 import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
057 import org.hd.d.pg2k.svrCore.Name;
058 import org.hd.d.pg2k.svrCore.TextUtils;
059 import org.hd.d.pg2k.webSvr.exhibit.ServletUtils;
060 import org.hd.d.pg2k.webSvr.exhibit.SimpleSimilarExhibitFinder;
061 import org.hd.d.pg2k.webSvr.util.WebUtils;
062
063 /**Tests of WAR utility routines, mainly from the WebUtils class. */
064 public final class WebUtilsTest extends TestCase
065 {
066 public WebUtilsTest(final String name)
067 {
068 super(name);
069 }
070
071 /**Tests of SimpleSimilarExhibitFinder class.
072 * This class is meant as the fast way to find some similar exhibits
073 * when the system is too busy for a full search to be practical.
074 * <p>
075 * The results of this simple search must be sane, if not always wonderful.
076 */
077 public void testSimpleSimilarExhibitFinder()
078 {
079 final Name.ExhibitFull THIS_EXNAME = Name.ExhibitFull.create("m/m-MM.jpg");
080
081 final ExhibitAttrUtils.ExhibitAttrWords attrWords = ExhibitAttrUtils.getAttrWords();
082
083 // Test on an empty List; test that we get a benign empty return.
084 final List<Name.ExhibitFull> emptyExhibitsList = Collections.emptyList();
085 final Set<String> emptyAttrWords = Collections.emptySet();
086 final Name.ExhibitFull r1[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
087 emptyExhibitsList, TextUtils.CASE_INSENSITIVE_ORDER, THIS_EXNAME, emptyAttrWords, Integer.MAX_VALUE);
088 assertTrue("result should not be null", r1 != null);
089 assertTrue("on empty input list result should be empty", r1.length == 0);
090
091 // Test on an List with only given exhibit; test that we get an empty return.
092 final Name.ExhibitFull r2[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
093 Collections.singletonList(THIS_EXNAME), TextUtils.CASE_INSENSITIVE_ORDER, THIS_EXNAME, emptyAttrWords, Integer.MAX_VALUE);
094 assertTrue("result should not be null", r2 != null);
095 assertTrue("on singleton input list result should be empty", r2.length == 0);
096
097 final long now = System.currentTimeMillis();
098 final Map<Name.ExhibitFull,ExhibitStaticAttr> exhibits1 = new HashMap<Name.ExhibitFull, ExhibitStaticAttr>();
099 exhibits1.put(THIS_EXNAME, new ExhibitStaticAttr(THIS_EXNAME, 100, now));
100 // final AllExhibitProperties aep1 = new AllExhibitProperties(
101 // new ExhibitPropsGlobalImmutable(),
102 // new LocationMap(),
103 // new AllExhibitImmutableData(exhibits1, now),
104 // Collections.EMPTY_MAP /* Map _loadedProps */);
105 final Name.ExhibitFull r3[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
106 Collections.singletonList(THIS_EXNAME), attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
107 assertTrue("result should not be null", r3 != null);
108 assertTrue("on singleton input list result should be empty", r3.length == 0);
109
110 // Add some more more entries with different main words.
111 // So this should still give no answers...
112 exhibits1.put(Name.ExhibitFull.create("a/a-AA.jpg"), new ExhibitStaticAttr(Name.ExhibitFull.create("a/a-AA.jpg"), 100, now));
113 exhibits1.put(Name.ExhibitFull.create("z/z-ZZ.jpg"), new ExhibitStaticAttr(Name.ExhibitFull.create("z/z-ZZ.jpg"), 100, now));
114 // final AllExhibitProperties aep4 = new AllExhibitProperties(
115 // new ExhibitPropsGlobalImmutable(),
116 // new LocationMap(),
117 // new AllExhibitImmutableData(exhibits1, now),
118 // Collections.EMPTY_MAP /* Map _loadedProps */);
119 final List<Name.ExhibitFull> l4 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
120 Collections.sort(l4, attrWords.SMART_ORDER);
121 final Name.ExhibitFull r4[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
122 l4, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
123 assertTrue("result should not be null", r4 != null);
124 assertTrue("on input list with only different main words result should be empty", r4.length == 0);
125
126 // Add a variant of the main exhibit name,
127 // which should not add any results...
128 final Name.ExhibitFull VARIANT1 = Name.ExhibitFull.create("m/m-bg-MM.jpg");
129 exhibits1.put(VARIANT1, new ExhibitStaticAttr(VARIANT1, 100, now));
130 // final AllExhibitProperties aep5 = new AllExhibitProperties(
131 // new ExhibitPropsGlobalImmutable(),
132 // new LocationMap(),
133 // new AllExhibitImmutableData(exhibits1, now),
134 // Collections.EMPTY_MAP /* Map _loadedProps */);
135 final List<Name.ExhibitFull> l5 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
136 Collections.sort(l5, attrWords.SMART_ORDER);
137 final Name.ExhibitFull r5[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
138 l5, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
139 assertTrue("result should not be null", r5 != null);
140 assertTrue("on input list with only variants and different main words result should be empty", r5.length == 0);
141
142 // Add a different exhibit with the same main word,
143 // which should now appear in the results...
144 final Name.ExhibitFull EXHIBIT2 = Name.ExhibitFull.create("m/m-yyy-MM.jpg");
145 exhibits1.put(EXHIBIT2, new ExhibitStaticAttr(EXHIBIT2, 100, now));
146 // final AllExhibitProperties aep6 = new AllExhibitProperties(
147 // new ExhibitPropsGlobalImmutable(),
148 // new LocationMap(),
149 // new AllExhibitImmutableData(exhibits1, now),
150 // Collections.EMPTY_MAP /* Map _loadedProps */);
151 final List<Name.ExhibitFull> l6 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
152 Collections.sort(l6, attrWords.SMART_ORDER);
153 final Name.ExhibitFull r6[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
154 l6, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
155 assertTrue("result should not be null", r6 != null);
156 assertTrue("on input list with one different exhibit should yield one result", r6.length == 1);
157 assertTrue("on input list with one different exhibit should yield that exhibit", TextUtils.contentEquals(EXHIBIT2, r6[0]));
158
159 // Add a second exhibit with the same main word,
160 // so both should now appear in the results...
161 final Name.ExhibitFull EXHIBIT3 = Name.ExhibitFull.create("m/m-www-MM.jpg");
162 exhibits1.put(EXHIBIT3, new ExhibitStaticAttr(EXHIBIT3, 100, now));
163 // final AllExhibitProperties aep7 = new AllExhibitProperties(
164 // new ExhibitPropsGlobalImmutable(),
165 // new LocationMap(),
166 // new AllExhibitImmutableData(exhibits1, now),
167 // Collections.EMPTY_MAP /* Map _loadedProps */);
168 final List<Name.ExhibitFull> l7 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
169 Collections.sort(l7, attrWords.SMART_ORDER);
170 final Name.ExhibitFull r7[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
171 l7, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
172 assertTrue("result should not be null", r7 != null);
173 assertTrue("on input list with two extra exhibits should yield two results", r7.length == 2);
174 assertTrue("both new exhibits should be in the results",
175 TextUtils.contentEquals(EXHIBIT3, r7[0]) &&
176 TextUtils.contentEquals(EXHIBIT2, r7[1]));
177
178 // Now limit the output size and check that all outputs are
179 // the new exhibits.
180 final Name.ExhibitFull r7b[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
181 l7, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), 1);
182 assertTrue("result should not be null", r7b != null);
183 assertTrue("output should be limited to requested length", r7b.length == 1);
184 assertTrue("both new exhibits should be in the results",
185 TextUtils.contentEquals(EXHIBIT3, r7b[0]) ||
186 TextUtils.contentEquals(EXHIBIT2, r7b[0]));
187
188 // Add a third exhibit with the same main word (but different in case),
189 // so both all should now appear in the results
190 // (comparator must be case-insensitive other than to break ties)...
191 final Name.ExhibitFull EXHIBIT4 = Name.ExhibitFull.create("m/M-vvv-MM.jpg");
192 exhibits1.put(EXHIBIT4, new ExhibitStaticAttr(EXHIBIT4, 100, now));
193 // final AllExhibitProperties aep8 = new AllExhibitProperties(
194 // new ExhibitPropsGlobalImmutable(),
195 // new LocationMap(),
196 // new AllExhibitImmutableData(exhibits1, now),
197 // Collections.EMPTY_MAP /* Map _loadedProps */);
198 final List<Name.ExhibitFull> l8 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
199 Collections.sort(l8, attrWords.SMART_ORDER);
200 final Name.ExhibitFull r8[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
201 l8, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
202 assertTrue("result should not be null", r8 != null);
203 assertTrue("expect comparator to be case-insensitive", r8.length == 3);
204 assertTrue("all new exhibits should be in the results",
205 TextUtils.contentEquals(EXHIBIT4, r8[0]) &&
206 TextUtils.contentEquals(EXHIBIT3, r8[1]) &&
207 TextUtils.contentEquals(EXHIBIT2, r8[2]));
208 }
209
210 /**Test of generation of short exhibit name prefix for titles.
211 * The prefixes generated must always be valid non-zero-length
212 * whole-word prefixes of the file components of the exhibit names
213 * selected, that make sense ti a human reader,
214 * but need not be totally perfect.
215 */
216 public void testMinimalUniqueENTitlePrefix()
217 {
218 // Test that a zero-length List always returns an empty string,
219 // even for an otherwise-invalid index.
220 final List<Name.ExhibitFull> emptyExhibitsList = Collections.emptyList();
221 assertTrue("On empty list, result should be harmless empty string.",
222 0 == WebUtils.minimalUniqueENTitlePrefix(emptyExhibitsList, -1).length());
223
224 // Our set of fake input names.
225 final List<Name.ExhibitFull> sampleNames = Arrays.asList(new Name.ExhibitFull[]
226 {
227 Name.ExhibitFull.create("zzz/apple-DHD.jpg"),
228 Name.ExhibitFull.create("yyy/apple-turnover-ANON.gif"),
229 Name.ExhibitFull.create("yyy/Apple-turnover-2-ANON.gif"),
230 Name.ExhibitFull.create("aaa-bbb/cat-1-ANON.gif"),
231 Name.ExhibitFull.create("ccc_ddd/cat-2-ANON.gif"),
232 Name.ExhibitFull.create("ccc-ddd/dog-2-ANON.gif"),
233 Name.ExhibitFull.create("ccc-ddd/dog-in-manger-2-ANON.gif"),
234 Name.ExhibitFull.create("ccc-ddd/dog-zeal-2-ANON.gif"),
235 Name.ExhibitFull.create("places-and-sights/_more1999/_more07/Indonesia-Bali-Ubud-Monkey-Forest-market-1-MB.jpg"),
236 Name.ExhibitFull.create("places-and-sights/_more1999/_more07/Indonesia-Bali-Ubud-Monkey-Forest-Pura-Dalem-ie-Temple-of-The-Dead-1-MB.jpg"),
237 Name.ExhibitFull.create("places-and-sights/_more1999/_more07/Indonesia-Bali-Ubud-Monkey-Forest-Pura-Dalem-ie-Temple-of-The-Dead-2-MB.jpg"),
238 Name.ExhibitFull.create("places-and-sights/_more1999/_more07/Indonesia-Bali-Ubud-Monkey-Forest-Pura-Dalem-ie-Temple-of-The-Dead-3-MB.jpg"),
239 Name.ExhibitFull.create("aaa/zebra-in-zoo-ANON.png")
240 });
241
242 // Expected abbreviation for each name.
243 final String expectedResponses[] =
244 {
245 "apple-",
246 "apple-turnover-",
247 "Apple-turnover-",
248 "cat-",
249 "cat-",
250 "dog-",
251 "dog-in-",
252 "dog-zeal-",
253 "Indonesia-Bali-Ubud-Monkey-Forest-market-",
254 "Indonesia-Bali-Ubud-Monkey-Forest-Pura-",
255 "Indonesia-Bali-Ubud-Monkey-Forest-Pura-",
256 "Indonesia-Bali-Ubud-Monkey-Forest-Pura-",
257 "zebra-"
258 };
259
260 // Make sure that our test data is self-consistent.
261 assertTrue("test set wrong size", expectedResponses.length == sampleNames.size());
262
263 // Test for friendly behaviour on bad inputs.
264 assertTrue("On negative (illegal) index, result should be harmless empty string.",
265 0 == WebUtils.minimalUniqueENTitlePrefix(sampleNames, -11111).length());
266 assertTrue("On over-large (illegal) index, result should be harmless empty string.",
267 0 == WebUtils.minimalUniqueENTitlePrefix(sampleNames, 3 + 2*sampleNames.size()).length());
268
269
270 // Test that we get the expected output for each index.
271 for(int i = sampleNames.size(); --i >= 0; )
272 {
273 final String actualResponse = WebUtils.minimalUniqueENTitlePrefix(sampleNames, i).toString();
274 assertTrue("unexpected response `"+actualResponse+"' at index "+i+
275 " instead of `"+(expectedResponses[i])+"'",
276 expectedResponses[i].equals(actualResponse));
277 }
278 }
279
280 /**Simple tests of WebUtils.sanitiseForXML().
281 * This tests for robust behaviour of this routine with and without
282 * special handling of i18n marked-up text.
283 */
284 public static final void testSimple_sanitiseForXML()
285 {
286 // Maximum length for sanitisation.
287 final int maxLen = 16;
288 // This data has three columns:
289 // 1) The input String.
290 // 2) The output String if entity codes are not allowed.
291 // 3) The output String if entity codes are allowed.
292 final String testData[][] =
293 {
294 // Empty string; no conversion needed.
295 { "", "", "" },
296
297 // Convert null input to empty-string result.
298 { null, "", "" },
299
300 // All whitespace; result is empty string.
301 { " \r \n \r\n \t\t", "", "" },
302
303 // Simple short pure alphanumeric: no transform needed.
304 { "abc1234Z", "abc1234Z", "abc1234Z" },
305
306 // Simple short text with whitespace: whitespace is trimmed.
307 { " hot dog! ", "hot dog!", "hot dog!" },
308
309 // Long string is trimmed then truncated.
310 { " a long long string ", "a long long ...", "a long long ..." },
311
312 // Test handling of a single entity.
313 { "Cow & Gate", "Cow +amp; Gate", "Cow & Gate" },
314
315 // Test handling of a single entity that seems to make the string too long.
316 { "&longbutvalidentitycode;", "+longbutvalid...", "&longbutvalidentitycode;" },
317
318 // Test handling of non-ASCII characters...
319 { "Mer\u0127ba ghal", "Mer ba ghal", "Merħba ghal" },
320 };
321
322 for(int i = testData.length; --i >= 0; )
323 {
324 final String input = testData[i][0];
325 final String expectedNEOutput = testData[i][1];
326 final String expectedEOutput = testData[i][2];
327
328 final String nEOutput = TextUtils.sanitiseForXML(input, maxLen, false);
329 assertTrue("no-entity-code output should be strictly length bounded",
330 nEOutput.length() <= maxLen);
331 assertTrue("unexpected no-entity-code result at index "+i+
332 " got `"+nEOutput+"', expected `"+expectedNEOutput+"'",
333 expectedNEOutput.equals(nEOutput));
334
335 final String eOutput = TextUtils.sanitiseForXML(input, maxLen, true);
336 assertTrue("unexpected entity-code result at index "+i+
337 " got `"+eOutput+"', expected `"+expectedEOutput+"'",
338 expectedEOutput.equals(eOutput));
339 }
340 }
341
342 /**Test bot/spider matching from UA name. */
343 public static void testUABotNameMatching()
344 {
345 if(WebUtils.UA_REGEX == null)
346 {
347 Main.getOut().println("UA_REGEX is null; skipping test...");
348 return;
349 }
350
351 assertFalse("Must not class vanilla browser (eg FF) as a bot", WebUtils.UA_REGEX.matcher("Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7").matches());
352 assertTrue("Must catch common bots (eg IA)", WebUtils.UA_REGEX.matcher("ia_archiver").matches());
353 assertTrue("Should match newly-created bot name", WebUtils.UA_REGEX.matcher("MyNewBadBot").matches());
354 }
355
356 private static final String TEST_STRONG_ETAG_VALUE = "\"123\"";
357 private static final String TEST_WEAK_ETAG_VALUE = "W/\"abc\"";
358
359 /**Test very basic If-None-Match / If-Modified-Since handling. */
360 public static void testINMIMSBasics()
361 {
362 // Check that request/response args are not even examined or used with LM/ETag unknown
363 // and the return is always false (don't abort, send the body).
364 assertFalse("with last modified == -1 no other args are even checked",
365 WebUtils.abortIfNotModifiedSince(-1, null, null));
366 assertFalse("with eTag == null and last modified == -1 no other args are even checked",
367 WebUtils.abortIfETagMatchOrNotModifiedSince(null, -1, null, null));
368 assertFalse("with last modified == -1 no other args are even checked",
369 WebUtils.abortIfNotModifiedSince(-1, new BaseMockHttpServletRequest(){}, new BaseMockHttpServletResponse(){}));
370 assertFalse("with eTag == null and last modified == -1 no other args are even checked",
371 WebUtils.abortIfETagMatchOrNotModifiedSince(null, -1, new BaseMockHttpServletRequest(){}, new BaseMockHttpServletResponse(){}));
372
373 // Check that with no If-Modified-Since or If-None-Match set
374 // that the return is always false (don't abort, send the body)
375 // for any reasonable/positive Last-Modified / ETag.
376 final HttpServletRequest noParamsRequest = new MockHttpServletRequestWithParams(Collections.<String,String>emptyMap());
377 assertFalse("with last modified == -1 no other args are even checked",
378 WebUtils.abortIfNotModifiedSince(System.currentTimeMillis(),
379 noParamsRequest,
380 new BaseMockHttpServletResponse()));
381 assertFalse("with eTag == null and last modified == -1 no other args are even checked",
382 WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_STRONG_ETAG_VALUE, System.currentTimeMillis(),
383 noParamsRequest,
384 new BaseMockHttpServletResponse()));
385 }
386
387 /**Mock response that accepts setting of status value. */
388 static final class MockHttpServletResponseThatAcceptsStatus extends BaseMockHttpServletResponse
389 {
390 /**Status value; null until set. */
391 Integer status;
392
393 @Override public void setStatus(final int arg0)
394 { status = Integer.valueOf(arg0); }
395 }
396
397 /**Test If-Modified-Since handling. */
398 public static void testIMS()
399 {
400 // Check that Last-Modified / If-Modified-Since behaves as expected.
401 // Create a time for the If-Modified-Since header a little in the past.
402 final long tIMS = System.currentTimeMillis() - 50000;
403 final SimpleDateFormat formatter = new SimpleDateFormat(MockHttpServletRequestWithParams.DATE_PATTERN_RFC1123, Locale.US);
404 formatter.setTimeZone(MockHttpServletRequestWithParams.GMT);
405 final String headerValueIMS = formatter.format(new Date(tIMS));
406 final HttpServletRequest request = new MockHttpServletRequestWithParams(Collections.singletonMap("If-Modified-Since", headerValueIMS));
407
408 // Check that if last-modified time is at least 1s before the IMS header,
409 // then the body should not be generated (ie client's current copy is up-to-date).
410 final MockHttpServletResponseThatAcceptsStatus response1 = new MockHttpServletResponseThatAcceptsStatus();
411 assertTrue("with last modified 1s before IMS, then body should not be sent",
412 WebUtils.abortIfNotModifiedSince(tIMS - 1000, request, response1));
413 assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response1.status));
414
415 // Check that if last-modified time is the same as the the IMS header,
416 // then the body should not be generated (ie client's current copy is up-to-date).
417 final MockHttpServletResponseThatAcceptsStatus response2 = new MockHttpServletResponseThatAcceptsStatus();
418 assertTrue("with last modified is same as IMS, then body should not be sent",
419 WebUtils.abortIfNotModifiedSince(tIMS, request, response2));
420 assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response2.status));
421
422 // Check that if last-modified time is at least 1s after the IMS header,
423 // then the body should be generated (ie client's current copy is not up-to-date).
424 final MockHttpServletResponseThatAcceptsStatus response3 = new MockHttpServletResponseThatAcceptsStatus();
425 assertFalse("with last modified 1s after as IMS, then body should be sent",
426 WebUtils.abortIfNotModifiedSince(tIMS + 1000, request, response3));
427 assertNull("correct stats should have been set", response3.status);
428 }
429
430 /**Test If-None-Match handling. */
431 public static void testINM()
432 {
433 // Check that ETag / If-None-Match behaves as expected.
434
435 // Check that an If-None-Match header of '*' always results in true (abort) when we have an ETag.
436 final HttpServletRequest request1 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", "*"));
437 final MockHttpServletResponseThatAcceptsStatus response1 = new MockHttpServletResponseThatAcceptsStatus();
438 assertTrue("with eTag != null (and last modified == -1), If-None-Match: * should force abort",
439 WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_STRONG_ETAG_VALUE, -1, request1, response1));
440 assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response1.status));
441
442 // Basic behaviour of underlying library support...
443 final BasicHeader bh1 = new BasicHeader("If-None-Match", "*");
444 assertEquals(1, bh1.getElements().length);
445 assertEquals("*", bh1.getElements()[0].toString());
446 final BasicHeader bh2 = new BasicHeader("If-None-Match", TEST_STRONG_ETAG_VALUE);
447 assertEquals(1, bh2.getElements().length);
448 assertEquals(TEST_STRONG_ETAG_VALUE, bh2.getElements()[0].toString());
449 final BasicHeader bh3 = new BasicHeader("If-None-Match", TEST_STRONG_ETAG_VALUE + ", " + TEST_WEAK_ETAG_VALUE);
450 assertEquals(2, bh3.getElements().length);
451 assertEquals(TEST_STRONG_ETAG_VALUE, bh3.getElements()[0].toString());
452 assertEquals(TEST_WEAK_ETAG_VALUE, bh3.getElements()[1].toString());
453
454 // Check that matching ETag results in true (abort).
455 final HttpServletRequest request2 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", TEST_WEAK_ETAG_VALUE));
456 final MockHttpServletResponseThatAcceptsStatus response2 = new MockHttpServletResponseThatAcceptsStatus();
457 assertTrue("with eTag set (and last modified == -1), If-None-Match: eTag should force abort",
458 WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_WEAK_ETAG_VALUE, -1, request2, response2));
459 assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response2.status));
460 final HttpServletRequest request3 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", TEST_STRONG_ETAG_VALUE + ", " + TEST_WEAK_ETAG_VALUE));
461 final MockHttpServletResponseThatAcceptsStatus response3 = new MockHttpServletResponseThatAcceptsStatus();
462 assertTrue("with eTag set (and last modified == -1), If-None-Match: eTag should force abort",
463 WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_WEAK_ETAG_VALUE, -1, request3, response3));
464 assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response3.status));
465
466 // Check that IMS ignored if INM is present.
467 // Follow ETag (abort) even if LM would not abort.
468 final long tIMS = System.currentTimeMillis() - 50000;
469 final SimpleDateFormat formatter = new SimpleDateFormat(MockHttpServletRequestWithParams.DATE_PATTERN_RFC1123, Locale.US);
470 formatter.setTimeZone(MockHttpServletRequestWithParams.GMT);
471 final String headerValueIMS = formatter.format(new Date(tIMS));
472 final Map<String, String> headers = new HashMap<String, String>();
473 headers.put("If-None-Match", TEST_WEAK_ETAG_VALUE);
474 headers.put("If-Modified-Since", headerValueIMS);
475 final HttpServletRequest request4 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", TEST_WEAK_ETAG_VALUE));
476 final MockHttpServletResponseThatAcceptsStatus response4 = new MockHttpServletResponseThatAcceptsStatus();
477 assertTrue("with eTag set (even with last modified newer), If-None-Match: eTag should force abort",
478 WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_WEAK_ETAG_VALUE, tIMS + 1000, request4, response4));
479 assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response4.status));
480 // Follow ETag (not abort) even if LM would abort.
481 headers.put("If-None-Match", TEST_WEAK_ETAG_VALUE);
482 headers.put("If-Modified-Since", headerValueIMS);
483 final HttpServletRequest request5 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", TEST_WEAK_ETAG_VALUE));
484 final MockHttpServletResponseThatAcceptsStatus response5 = new MockHttpServletResponseThatAcceptsStatus();
485 assertFalse("with eTag set (even with last modified newer), If-None-Match: eTag should force abort",
486 WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_STRONG_ETAG_VALUE, tIMS - 1000, request5, response5));
487 assertNull("stats should not have been set", response5.status);
488 }
489
490 /**Test our sanitisation of referrer names... */
491 public static final void testReferrerHostNameSanitisation()
492 {
493 final String benign = "my.host.com";
494 assertEquals("benign lower-case name should be left alone", benign, ServletUtils.sanitiseReferrerHostName(benign));
495 final String ucBenign = "UC.host.com";
496 assertEquals("benign (part) upper-case name should be lower-cased", ucBenign.toLowerCase(), ServletUtils.sanitiseReferrerHostName(ucBenign));
497 final String badChars = "$\u0fff\n%";
498 assertEquals("bad chars should be replaced with ?", "????", ServletUtils.sanitiseReferrerHostName(badChars));
499 final String veryLong = "abcdefghijklmnopqrstuvwxyz.tediously.long.DOM.ain";
500 final String sVeryLong = ServletUtils.sanitiseReferrerHostName(veryLong);
501 assertTrue("very long domain should be truncated on left", sVeryLong.endsWith(".dom.ain"));
502 assertTrue("very long domain should start with '.' to indicate truncation", sVeryLong.startsWith("."));
503 assertTrue("very long domain should sanitised to lower-cased substring", veryLong.toLowerCase().endsWith(sVeryLong));
504 assertTrue("long IPv6 literal not truncated on left", ServletUtils.sanitiseReferrerHostName("[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]").startsWith("[fedc:"));
505 assertEquals("short IPv6 literal with embedded IPv4 not truncated", ServletUtils.sanitiseReferrerHostName("[::192.9.5.5]"), "[::192.9.5.5]");
506 }
507 }