/* * Copyright (c) 2008. All rights reserved. */ package ro.isdc.wro.manager; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.Writer; import java.net.URL; import java.util.Arrays; import javax.servlet.FilterConfig; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.output.WriterOutputStream; import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ro.isdc.wro.WroRuntimeException; import ro.isdc.wro.cache.CacheKey; import ro.isdc.wro.cache.CacheStrategy; import ro.isdc.wro.cache.CacheValue; import ro.isdc.wro.config.Context; import ro.isdc.wro.config.jmx.WroConfiguration; import ro.isdc.wro.http.support.DelegatingServletOutputStream; import ro.isdc.wro.http.support.HttpHeader; import ro.isdc.wro.manager.callback.LifecycleCallback; import ro.isdc.wro.manager.callback.PerformanceLoggerCallback; import ro.isdc.wro.manager.factory.BaseWroManagerFactory; import ro.isdc.wro.manager.factory.NoProcessorsWroManagerFactory; import ro.isdc.wro.manager.factory.WroManagerFactory; import ro.isdc.wro.manager.runnable.ReloadModelRunnable; import ro.isdc.wro.model.WroModel; import ro.isdc.wro.model.factory.WroModelFactory; import ro.isdc.wro.model.factory.XmlModelFactory; import ro.isdc.wro.model.group.DefaultGroupExtractor; import ro.isdc.wro.model.group.Group; import ro.isdc.wro.model.group.GroupExtractor; import ro.isdc.wro.model.group.processor.Injector; import ro.isdc.wro.model.group.processor.InjectorBuilder; import ro.isdc.wro.model.resource.Resource; import ro.isdc.wro.model.resource.ResourceType; import ro.isdc.wro.model.resource.processor.Destroyable; import ro.isdc.wro.model.resource.processor.ResourcePreProcessor; import ro.isdc.wro.model.resource.processor.factory.SimpleProcessorsFactory; import ro.isdc.wro.model.resource.processor.impl.PlaceholderProcessor; import ro.isdc.wro.model.resource.support.MutableResourceAuthorizationManager; import ro.isdc.wro.model.resource.support.hash.CRC32HashStrategy; import ro.isdc.wro.model.resource.support.hash.MD5HashStrategy; import ro.isdc.wro.util.AbstractDecorator; import ro.isdc.wro.util.ObjectFactory; import ro.isdc.wro.util.WroTestUtils; import ro.isdc.wro.util.WroUtil; import ro.isdc.wro.util.io.UnclosableBufferedInputStream; /** * TestWroManager.java. * * @author Alex Objelean * @created Created on Nov 3, 2008 */ public class TestWroManager { private static final Logger LOG = LoggerFactory.getLogger(TestWroManager.class); @Mock private MutableResourceAuthorizationManager mockAuthorizationManager; @Mock private CacheStrategy<CacheKey, CacheValue> mockCacheStrategy; @Mock private WroModelFactory mockModelFactory; /** * Used to test simple operations. */ private WroManager victim; /** * Used to test more complex use-cases. */ private WroManagerFactory managerFactory; @BeforeClass public static void onBeforeClass() { assertEquals(0, Context.countActive()); } @AfterClass public static void onAfterClass() { assertEquals(0, Context.countActive()); } @Before public void setUp() { MockitoAnnotations.initMocks(this); final Context context = Context.webContext(Mockito.mock(HttpServletRequest.class), Mockito.mock(HttpServletResponse.class, Mockito.RETURNS_DEEP_STUBS), Mockito.mock(FilterConfig.class)); Context.set(context, newConfigWithUpdatePeriodValue(0)); managerFactory = new BaseWroManagerFactory().setModelFactory(getValidModelFactory()).setResourceAuthorizationManager( mockAuthorizationManager); final Injector injector = new InjectorBuilder(managerFactory).build(); victim = managerFactory.create(); injector.inject(victim); } /** * A processor which which uses a {@link WroManager} during processor, in order to process a single group, whose * resource is the pre processed resource of this processor. */ private static final class WroManagerProcessor implements ResourcePreProcessor { private BaseWroManagerFactory createManagerFactory(final Resource resource) { return new BaseWroManagerFactory() { @Override protected void onAfterInitializeManager(final WroManager manager) { manager.registerCallback(new ObjectFactory<LifecycleCallback>() { public LifecycleCallback create() { return new PerformanceLoggerCallback(); } }); }; @Override protected WroModelFactory newModelFactory() { return WroTestUtils.simpleModelFactory(new WroModel().addGroup(new Group("group").addResource(resource))); } }; } public void process(final Resource resource, final Reader reader, final Writer writer) throws IOException { LOG.debug("resource: {}", resource); final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Context.get().getResponse(); Mockito.when(response.getOutputStream()).thenReturn( new DelegatingServletOutputStream(new WriterOutputStream(writer))); Mockito.when(request.getRequestURI()).thenReturn(""); final WroConfiguration config = new WroConfiguration(); // we don't need caching here, otherwise we'll have clashing during unit tests. config.setDebug(true); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class)), config); // create a groupExtractor which always return the same group name. final String groupName = "group"; final GroupExtractor groupExtractor = new DefaultGroupExtractor() { @Override public String getGroupName(final HttpServletRequest request) { return groupName; } @Override public ResourceType getResourceType(final HttpServletRequest request) { return resource.getType(); } }; // this manager will make sure that we always process a model holding one group which has only one resource. final WroManagerFactory managerFactory = createManagerFactory(resource).setGroupExtractor(groupExtractor); managerFactory.create().process(); } } private class GenericTestBuilder { /** * Perform a processing on a group extracted from requestUri and compares with the expectedResourceUri content. * * @param requestUri * contains the group name to process. * @param expectedResourceUri * the uri of the resource which has the expected content. */ public void processAndCompare(final String requestUri, final String expectedResourceUri) throws Exception { final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Context.get().getResponse(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); Mockito.when(response.getOutputStream()).thenReturn(new DelegatingServletOutputStream(out)); Mockito.when(request.getRequestURI()).thenReturn(requestUri); final WroConfiguration config = new WroConfiguration(); config.setIgnoreFailingProcessor(true); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class)), config); onBeforeProcess(); managerFactory.create().process(); // compare written bytes to output stream with the content from specified css. final InputStream expectedInputStream = new UnclosableBufferedInputStream( WroTestUtils.getInputStream(expectedResourceUri)); final InputStream actualInputStream = new BufferedInputStream(new ByteArrayInputStream(out.toByteArray())); expectedInputStream.reset(); WroTestUtils.compare(expectedInputStream, actualInputStream); expectedInputStream.close(); actualInputStream.close(); } /** * Allow to execute custom logic before the actual processing is done. */ protected void onBeforeProcess() { } } /** * @return a {@link XmlModelFactory} pointing to a valid config resource. */ private static XmlModelFactory getValidModelFactory() { return new XmlModelFactory() { @Override protected InputStream getModelResourceAsStream() { return TestWroManager.class.getResourceAsStream("wro.xml"); } }; } @Test(expected = NullPointerException.class) public void cannotRegisterNullCallback() { final WroManager manager = new BaseWroManagerFactory().create(); manager.registerCallback(null); } /** * Ignored because it fails when running the test from command line. */ @Test public void testFromFolder() throws Exception { final ResourcePreProcessor processor = new WroManagerProcessor(); final URL url = getClass().getResource("wroManager"); final File testFolder = new File(url.getFile(), "test"); final File expectedFolder = new File(url.getFile(), "expected"); WroTestUtils.compareFromDifferentFoldersByExtension(testFolder, expectedFolder, "js", processor); } /** * Initialize {@link WroConfiguration} object with cacheUpdatePeriod & modelUpdatePeriod equal with provided argument. */ private WroConfiguration newConfigWithUpdatePeriodValue(final long periodValue) { final WroConfiguration config = new WroConfiguration(); config.setCacheUpdatePeriod(periodValue); config.setModelUpdatePeriod(periodValue); return config; } @Test public void shouldClearAuthorizationManagerWhenCachePeriodChanged() { victim.onCachePeriodChanged(1); verify(mockAuthorizationManager, atLeastOnce()).clear(); } @Test public void shouldClearAuthorizationManagerWhenModelPeriodChanged() { victim.onModelPeriodChanged(1); verify(mockAuthorizationManager, atLeastOnce()).clear(); } @Test public void testNoProcessorWroManagerFactory() throws IOException { final WroManagerFactory factory = new NoProcessorsWroManagerFactory().setModelFactory(getValidModelFactory()); final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Context.get().getResponse(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); Mockito.when(response.getOutputStream()).thenReturn(new DelegatingServletOutputStream(out)); Mockito.when(request.getRequestURI()).thenReturn("/app/g1.css"); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class))); factory.create().process(); // compare written bytes to output stream with the content from specified css. WroTestUtils.compare(WroTestUtils.getInputStream("classpath:ro/isdc/wro/manager/noProcessorsResult.css"), new ByteArrayInputStream(out.toByteArray())); } @Test public void testDuplicatedResourcesShouldBeSkipped() throws Exception { new GenericTestBuilder().processAndCompare("/repeatedResources.js", "classpath:ro/isdc/wro/manager/repeated-out.js"); } @Test public void testWildcardDuplicatedResourcesShouldBeSkiped() throws Exception { new GenericTestBuilder().processAndCompare("/wildcardRepeatedResources.js", "classpath:ro/isdc/wro/manager/wildcardRepeated-out.js"); } @Test public void testMinimizeAttributeIsFalseOnResource() throws Exception { new GenericTestBuilder().processAndCompare("/resourceMinimizeFalse.js", "classpath:ro/isdc/wro/manager/sample.js"); } @Test public void testMinimizeAttributeIsTrueOnResource() throws Exception { new GenericTestBuilder().processAndCompare("/resourceMinimizeTrue.js", "classpath:ro/isdc/wro/manager/sample.min.js"); } @Test public void testWildcardGroupResources() throws Exception { new GenericTestBuilder().processAndCompare("/wildcardResources.js", "classpath:ro/isdc/wro/manager/wildcard-out.js"); } /** * Test that when ignoreMissingResource is true and IOException is thrown by a processor, no exception is thrown. */ @Test public void testCssWithInvalidImport() throws Exception { new GenericTestBuilder().processAndCompare("/invalidImport.css", "classpath:ro/isdc/wro/manager/invalidImport-out.css"); } @Test(expected = WroRuntimeException.class) public void shouldNotIgnoreInvalidImportWhenImportedResourceIsMissing() throws Exception { genericIgnoreMissingResourceTest(false); } @Test public void shouldIgnoreInvalidImportWhenImportedResourceIsMissing() throws Exception { genericIgnoreMissingResourceTest(true); } private void genericIgnoreMissingResourceTest(final boolean ignoreFlag) throws Exception { new GenericTestBuilder() { @Override protected void onBeforeProcess() { final WroConfiguration config = Context.get().getConfig(); config.setIgnoreFailingProcessor(ignoreFlag); config.setIgnoreMissingResources(ignoreFlag); }; }.processAndCompare("/invalidImport.css", "classpath:ro/isdc/wro/manager/invalidImport-out.css"); } @Test public void processValidModel() throws IOException { final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Mockito.mock(HttpServletResponse.class, Mockito.RETURNS_DEEP_STUBS); Mockito.when(request.getRequestURI()).thenReturn("/app/g1.css"); // Test also that ETag header value contains quotes Mockito.doAnswer(new Answer<Void>() { public Void answer(final InvocationOnMock invocation) throws Throwable { LOG.debug("Header: {}", Arrays.toString(invocation.getArguments())); final Object[] arguments = invocation.getArguments(); if (HttpHeader.ETAG.toString().equals(arguments[0])) { final String etagHeaderValue = (String) arguments[1]; Assert.assertTrue(etagHeaderValue.matches("\".*?\"")); } return null; } }).when(response).setHeader(Mockito.anyString(), Mockito.anyString()); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class))); managerFactory.create().process(); } @Test public void testManagerWithSchedulerAndUpdatePeriodSet() throws Exception { final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); Mockito.when(request.getRequestURI()).thenReturn("/app/g1.css"); final Context context = Context.webContext(request, Mockito.mock(HttpServletResponse.class, Mockito.RETURNS_DEEP_STUBS), Mockito.mock(FilterConfig.class)); final WroConfiguration config = new WroConfiguration(); // make it run each 1 second config.setModelUpdatePeriod(1); config.setCacheUpdatePeriod(1); Context.unset(); Context.set(context, config); managerFactory.create().process(); // let scheduler run a while Thread.sleep(1100); } /** * Test how manager behaves when the update period value is greater than zero and the scheduler starts. * * @throws Exception */ @Test public void testManagerWhenSchedulerIsStarted() throws Exception { newConfigWithUpdatePeriodValue(1); final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Context.get().getResponse(); Mockito.when(request.getRequestURI()).thenReturn("/app/g1.css"); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class))); managerFactory.create().process(); // allow thread to do its job Thread.sleep(300); } @Test public void testAggregatedComputedFolder() throws Exception { final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Context.get().getResponse(); Mockito.when(request.getRequestURI()).thenReturn("/wro4j/wro/g1.css"); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class))); managerFactory.create().process(); Assert.assertEquals("/wro4j/wro/", Context.get().getAggregatedFolderPath()); } @Test public void testAggregatedComputedFolder2() throws Exception { final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Context.get().getResponse(); Mockito.when(request.getRequestURI()).thenReturn("/wro4j/wro/path/to/g1.css"); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class))); managerFactory.create().process(); Assert.assertEquals("/wro4j/wro/path/to/", Context.get().getAggregatedFolderPath()); } @Test(expected = WroRuntimeException.class) public void shouldNotProcessGroupWithNoResources() throws Exception { final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Context.get().getResponse(); Mockito.when(request.getRequestURI()).thenReturn("/noResources.css"); final WroConfiguration config = new WroConfiguration(); config.setIgnoreEmptyGroup(false); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class)), config); final WroModel model = new WroModel(); model.addGroup(new Group("noResources")); final WroManagerFactory managerFactory = new BaseWroManagerFactory().setModelFactory(WroUtil.factoryFor(model)); managerFactory.create().process(); } @Test public void testCRC32Fingerprint() throws Exception { final WroManagerFactory factory = new BaseWroManagerFactory().setModelFactory(getValidModelFactory()).setHashStrategy( new CRC32HashStrategy()); final WroManager manager = factory.create(); final String path = manager.encodeVersionIntoGroupPath("g3", ResourceType.CSS, true); assertEquals("1d62dbaf/g3.css?minimize=true", path); } @Test public void testMD5Fingerprint() throws Exception { final WroManagerFactory factory = new BaseWroManagerFactory().setModelFactory(getValidModelFactory()).setHashStrategy( new MD5HashStrategy()); final WroManager manager = factory.create(); final String path = manager.encodeVersionIntoGroupPath("g3", ResourceType.CSS, true); assertEquals("9394d6cdd0a75a5f695c84eda410103f/g3.css?minimize=true", path); } @Test public void testSHA1DefaultHashBuilder() throws Exception { final WroManager manager = managerFactory.create(); final String path = manager.encodeVersionIntoGroupPath("g3", ResourceType.CSS, true); assertEquals("aebaedcdec8131230d47259be7a628b5dfeff6ba/g3.css?minimize=true", path); } @Test public void cacheShouldNotBeClearedAfterModelReload() throws IOException { final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); final HttpServletResponse response = Mockito.mock(HttpServletResponse.class, Mockito.RETURNS_DEEP_STUBS); Mockito.when(request.getRequestURI()).thenReturn("/app/g3.css"); final WroConfiguration config = new WroConfiguration(); config.setDebug(true); Context.unset(); Context.set(Context.webContext(request, response, Mockito.mock(FilterConfig.class))); final WroManager wroManager = managerFactory.create(); wroManager.process(); // use original decorated object because the decorated one trigger the processing for each cache lookup. final CacheStrategy<CacheKey, CacheValue> cacheStrategy = AbstractDecorator.getOriginalDecoratedObject(wroManager.getCacheStrategy()); Assert.assertNotNull(cacheStrategy.get(new CacheKey("g3", ResourceType.CSS, true))); final ReloadModelRunnable reloadModelRunnable = new ReloadModelRunnable(wroManager.getModelFactory()); reloadModelRunnable.run(); Assert.assertNotNull(cacheStrategy.get(new CacheKey("g3", ResourceType.CSS, true))); } @Test public void shouldRegisterCallback() { final LifecycleCallback mockCallback = Mockito.mock(LifecycleCallback.class); victim.registerCallback(new ObjectFactory<LifecycleCallback>() { public LifecycleCallback create() { return mockCallback; } }); victim.getCallbackRegistry().onProcessingComplete(); Mockito.verify(mockCallback, Mockito.atLeastOnce()).onProcessingComplete(); } @Test public void shouldDestroyDependenciesWhenDestoryed() { final WroManager manager = new WroManager.Builder().setCacheStrategy(mockCacheStrategy).setModelFactory( mockModelFactory).build(); manager.destroy(); verify(mockCacheStrategy).destroy(); verify(mockModelFactory).destroy(); } private static class DestroyableProcessor extends PlaceholderProcessor implements Destroyable { public void destroy() { } } @Test public void shouldDestroyDestroyableProcessorWhenManagerIsDestroyed() { final DestroyableProcessor preProcessor = Mockito.mock(DestroyableProcessor.class); final DestroyableProcessor postProcessor = Mockito.mock(DestroyableProcessor.class); victim = new BaseWroManagerFactory().setProcessorsFactory( new SimpleProcessorsFactory().addPreProcessor(preProcessor).addPostProcessor(postProcessor)).create(); victim.destroy(); Mockito.verify(preProcessor, Mockito.times(1)).destroy(); Mockito.verify(postProcessor, Mockito.times(1)).destroy(); } @After public void tearDown() { managerFactory.destroy(); Context.unset(); } }