View Javadoc

1   package com.atlassian.plugin.osgi.factory;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.net.URL;
7   import java.net.URLConnection;
8   import java.net.URLStreamHandler;
9   import java.util.Hashtable;
10  import java.util.concurrent.TimeUnit;
11  
12  import com.atlassian.plugin.IllegalPluginStateException;
13  import com.atlassian.plugin.JarPluginArtifact;
14  import com.atlassian.plugin.Plugin;
15  import com.atlassian.plugin.PluginArtifact;
16  import com.atlassian.plugin.osgi.container.OsgiContainerException;
17  import com.atlassian.plugin.osgi.container.OsgiContainerManager;
18  import com.atlassian.plugin.test.PluginJarBuilder;
19  
20  import com.google.common.collect.ImmutableMap;
21  
22  import org.junit.Before;
23  import org.junit.Rule;
24  import org.junit.Test;
25  import org.junit.rules.ExpectedException;
26  import org.junit.runner.RunWith;
27  import org.mockito.Mock;
28  import org.mockito.runners.MockitoJUnitRunner;
29  import org.osgi.framework.Bundle;
30  import org.osgi.framework.BundleContext;
31  import org.osgi.framework.Constants;
32  
33  import static com.atlassian.plugin.PluginArtifact.AllowsReference.ReferenceMode.PERMIT_REFERENCE;
34  import static org.hamcrest.MatcherAssert.assertThat;
35  import static org.hamcrest.Matchers.greaterThanOrEqualTo;
36  import static org.hamcrest.Matchers.is;
37  import static org.hamcrest.Matchers.lessThanOrEqualTo;
38  import static org.hamcrest.Matchers.nullValue;
39  import static org.hamcrest.Matchers.sameInstance;
40  import static org.mockito.Mockito.mock;
41  import static org.mockito.Mockito.verify;
42  import static org.mockito.Mockito.when;
43  import static org.mockito.Mockito.withSettings;
44  
45  // An IntelliJ pattern to run both tests in this class
46  // com.atlassian.plugin.osgi.factory.TestOsgiBundlePlugin$Modern||com.atlassian.plugin.osgi.factory.TestOsgiBundlePlugin$Legacy
47  
48  /**
49   * Tests for OsgiBundlePlugin.
50   * <p/>
51   * This is an abstract base class for the common functionality between instances created with the preferred constructor {@link
52   * OsgiBundlePlugin#OsgiBundlePlugin(OsgiContainerManager, String, PluginArtifact)} and the legacy constructor {@link
53   * OsgiBundlePlugin#OsgiBundlePlugin(Bundle, String, PluginArtifact)}. Static inner classes subclass it to configure for the
54   * actual tests, and to add additional tests.
55   */
56  @RunWith (MockitoJUnitRunner.class)
57  public abstract class TestOsgiBundlePlugin
58  {
59      public static final String DESCRIPTION = "A Bundle for Testing";
60      public static final String NAME = "Test Bundle";
61      public static final String SYMBOLIC_NAME = TestOsgiBundlePlugin.class.getName() + ".testBundle";
62      public static final String VENDOR = "Some Bundle Vendor";
63      public static final String VERSION = "1.2";
64      public static final String PLUGIN_KEY = SYMBOLIC_NAME + "-" + VERSION;
65  
66      private long testStarted;
67  
68      @Mock Bundle bundle;
69      PluginArtifact pluginArtifact;
70  
71      OsgiBundlePlugin osgiBundlePlugin;
72  
73      @Before
74      public void setUp() throws Exception
75      {
76          testStarted = System.currentTimeMillis();
77          when(bundle.getHeaders()).thenReturn(new Hashtable<String, String>(getBundleManifest()));
78          pluginArtifact = getBundleJarPluginArtifact();
79          osgiBundlePlugin = getOsgiBundlePlugin();
80      }
81  
82      protected abstract OsgiBundlePlugin getOsgiBundlePlugin();
83  
84      @Test
85      public void constructorConfiguresPluginCorrectly()
86      {
87          assertThatPluginIsConfigured(osgiBundlePlugin);
88      }
89  
90      @Test
91      public void getDateLoadedIsRecent()
92      {
93          final long timeLoaded = osgiBundlePlugin.getDateLoaded().getTime();
94          final long now = System.currentTimeMillis();
95          assertThat(timeLoaded, greaterThanOrEqualTo(testStarted));
96          assertThat(timeLoaded, lessThanOrEqualTo(now));
97      }
98  
99      @Test
100     public void getDateInstalledMatchesFileTimestamp()
101     {
102         final long timeInstalled = osgiBundlePlugin.getDateInstalled().getTime();
103         assertThat(timeInstalled, is(pluginArtifact.toFile().lastModified()));
104     }
105 
106     @Test
107     public void isUninstallableIsTrue()
108     {
109         assertThat(osgiBundlePlugin.isUninstallable(), is(true));
110     }
111 
112     @Test
113     public void isDeleteableIsTrue()
114     {
115         assertThat(osgiBundlePlugin.isDeleteable(), is(true));
116     }
117 
118     @Test
119     public void isDynamicallyLoadedIsTrue()
120     {
121         assertThat(osgiBundlePlugin.isDynamicallyLoaded(), is(true));
122     }
123 
124     @Test
125     public void loadClassForwardsToBundle() throws Exception
126     {
127         osgiBundlePlugin.install();
128         // We're not stressing the class loader here, just checking the api works.
129         final String className = "java.lang.String";
130         final Class expected = String.class;
131         when(bundle.loadClass(className)).thenReturn(expected);
132         final Class actual = (Class) osgiBundlePlugin.loadClass(className, getClass());
133         verify(bundle).loadClass(className);
134         assertThat(actual, sameInstance(expected));
135     }
136 
137     @Test
138     public void getResourceForwardsToBundle() throws Exception
139     {
140         osgiBundlePlugin.install();
141         final String resourceName = "someResource";
142         final URL expected = new URL("mock", null, 0, "some/resource", mock(URLStreamHandler.class));
143         when(bundle.getResource(resourceName)).thenReturn(expected);
144         final URL actual = osgiBundlePlugin.getResource(resourceName);
145         verify(bundle).getResource(resourceName);
146         assertThat(actual, sameInstance(expected));
147     }
148 
149     @Test
150     public void getResourceAsStreamForwardsToBundle() throws Exception
151     {
152         osgiBundlePlugin.install();
153         final String resourceName = "someResource";
154         final InputStream expected = mock(InputStream.class);
155         final URLConnection urlConnection = mock(URLConnection.class);
156         when(urlConnection.getInputStream()).thenReturn(expected);
157         final URLStreamHandler urlStreamHandler = new URLStreamHandler()
158         {
159             @Override
160             protected URLConnection openConnection(final URL u) throws IOException
161             {
162                 return urlConnection;
163             }
164         };
165         final URL resourceUrl = new URL("mock", null, 0, "some/resource", urlStreamHandler);
166         when(bundle.getResource(resourceName)).thenReturn(resourceUrl);
167         final InputStream actual = osgiBundlePlugin.getResourceAsStream(resourceName);
168         verify(bundle).getResource(resourceName);
169         assertThat(actual, sameInstance(expected));
170     }
171 
172     @Test
173     public void getClassLoaderReturnsClassLoaderThatUsesBundle() throws Exception
174     {
175         osgiBundlePlugin.install();
176         // Need a class name which won't be owned by a parent class loader
177         final String className = "com.atlassian.plugin.test.some.fake.class.name";
178         // Doesn't really matter what class we use, but we can't mock a final like Class
179         final Class expected = String.class;
180         when(bundle.loadClass(className)).thenReturn(expected);
181         final Class actual = osgiBundlePlugin.getClassLoader().loadClass(className);
182         verify(bundle).loadClass(className);
183         assertThat(actual, sameInstance(expected));
184     }
185 
186     @Test
187     public void getPluginArtifactReturnsPluginArtifact()
188     {
189         assertThat(osgiBundlePlugin.getPluginArtifact(), is(pluginArtifact));
190     }
191 
192     @Test
193     public void executeBasicLifeCycle() throws Exception
194     {
195         final BundleContext bundleContext = mock(BundleContext.class);
196         when(bundle.getBundleContext()).thenReturn(bundleContext);
197         // Just a smoke test that we can drive the plugin through the typical states
198         osgiBundlePlugin.install();
199         osgiBundlePlugin.enable();
200         verify(bundle).start();
201         when(bundle.getState()).thenReturn(Bundle.ACTIVE);
202         osgiBundlePlugin.disable();
203         verify(bundle).stop();
204         osgiBundlePlugin.uninstall();
205     }
206 
207     static void assertThatPluginIsConfigured(final Plugin plugin)
208     {
209         assertThat(plugin.getKey(), is(PLUGIN_KEY));
210         assertThat(plugin.getPluginInformation().getDescription(), is(DESCRIPTION));
211         assertThat(plugin.getName(), is(NAME));
212         assertThat(plugin.getPluginInformation().getVendorName(), is(VENDOR));
213         assertThat(plugin.getPluginInformation().getVersion(), is(VERSION));
214         assertThat(plugin.getI18nNameKey(), nullValue());
215     }
216 
217     static PluginArtifact getBundleJarPluginArtifact() throws IOException
218     {
219         // For historic and convenience reasons, we just make a test jar rather than mocking out PluginArtifact
220         final File bundleJar = new PluginJarBuilder("somebundle")
221                 .manifest(getBundleManifest())
222                 .build();
223         // Backdate the jar so we can meaningfully test getDateInstalled vs getDateLoaded
224         if (!bundleJar.setLastModified(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)))
225         {
226             throw new IOException("Test broken, cannot backdate bundleJar '" + bundleJar + "'");
227         }
228         return new JarPluginArtifact(bundleJar);
229     }
230 
231     static ImmutableMap<String, String> getBundleManifest()
232     {
233         return ImmutableMap.<String, String>builder()
234                 .put(Constants.BUNDLE_DESCRIPTION, DESCRIPTION)
235                 .put(Constants.BUNDLE_NAME, NAME)
236                 .put(Constants.BUNDLE_SYMBOLICNAME, SYMBOLIC_NAME)
237                 .put(Constants.BUNDLE_VENDOR, VENDOR)
238                 .put(Constants.BUNDLE_VERSION, VERSION)
239                 .build();
240     }
241 
242     public static class Modern extends TestOsgiBundlePlugin
243     {
244         @Rule
245         public ExpectedException expectedException = ExpectedException.none();
246 
247         @Mock private OsgiContainerManager osgiContainerManager;
248 
249         @Override
250         public void setUp() throws Exception
251         {
252             super.setUp();
253             // Ensure that we can install the plugin
254             when(osgiContainerManager.installBundle(pluginArtifact.toFile())).thenReturn(bundle);
255         }
256 
257         @Override
258         protected OsgiBundlePlugin getOsgiBundlePlugin()
259         {
260             return new OsgiBundlePlugin(osgiContainerManager, PLUGIN_KEY, pluginArtifact);
261         }
262 
263         @Test
264         public void loadClassThrowsBeforeInstall() throws Exception
265         {
266             expectedException.expect(IllegalPluginStateException.class);
267             osgiBundlePlugin.loadClass("java.lang.String", getClass());
268         }
269 
270         @Test
271         public void getResourceThrowsBeforeInstall() throws Exception
272         {
273             expectedException.expect(IllegalPluginStateException.class);
274             osgiBundlePlugin.getResource("someResource");
275         }
276 
277         @Test
278         public void getResourceAsStreamThrowsBeforeInstall() throws Exception
279         {
280             expectedException.expect(IllegalPluginStateException.class);
281             osgiBundlePlugin.getResourceAsStream("someResource");
282         }
283 
284         @Test
285         public void getClassLoaderThrowsBeforeInstall() throws Exception
286         {
287             expectedException.expect(IllegalPluginStateException.class);
288             osgiBundlePlugin.getClassLoader();
289         }
290 
291         @Test
292         public void loadClassThrowsAfterUninstall() throws Exception
293         {
294             osgiBundlePlugin.install();
295             osgiBundlePlugin.uninstall();
296             expectedException.expect(IllegalPluginStateException.class);
297             osgiBundlePlugin.loadClass("java.lang.String", getClass());
298         }
299 
300         @Test
301         public void getResourceThrowsAfterUninstall() throws Exception
302         {
303             osgiBundlePlugin.install();
304             osgiBundlePlugin.uninstall();
305             expectedException.expect(IllegalPluginStateException.class);
306             osgiBundlePlugin.getResource("someResource");
307         }
308 
309         @Test
310         public void getResourceAsStreamThrowsAfterUninstall() throws Exception
311         {
312             osgiBundlePlugin.install();
313             osgiBundlePlugin.uninstall();
314             expectedException.expect(IllegalPluginStateException.class);
315             osgiBundlePlugin.getResourceAsStream("someResource");
316         }
317 
318         @Test
319         public void getClassLoaderThrowsAfterUninstall() throws Exception
320         {
321             osgiBundlePlugin.install();
322             osgiBundlePlugin.uninstall();
323             expectedException.expect(IllegalPluginStateException.class);
324             osgiBundlePlugin.getClassLoader();
325         }
326 
327         @Test
328         public void installFailsIfOsgiContainerManagerInstallBundleFails()
329         {
330             final OsgiContainerException osgiContainerException = new OsgiContainerException("Intentional fail for test");
331             when(osgiContainerManager.installBundle(pluginArtifact.toFile())).thenThrow(osgiContainerException);
332             // For now expect the exception to be directly propagated. Given that the type matches what our container throws,
333             // and what OsgiPlugin (rethrows), this type should be ok, we can rethink if we need to wrap / adjust the exception.
334             expectedException.expect(is(osgiContainerException));
335             osgiBundlePlugin.install();
336         }
337 
338         @Test
339         public void pluginArtifactThatAllowsReferenceIsInstalledByReference() throws Exception
340         {
341             final File bundleJar = pluginArtifact.toFile();
342             pluginArtifact = new JarPluginArtifact(bundleJar, PERMIT_REFERENCE);
343             osgiContainerManager = mock(OsgiContainerManager.class,
344                     withSettings().extraInterfaces(OsgiContainerManager.AllowsReferenceInstall.class));
345             // Have our mock OsgiContainerManager respond only to a reference install (the true in the next line)
346             when(((OsgiContainerManager.AllowsReferenceInstall) osgiContainerManager)
347                     .installBundle(bundleJar, true)).thenReturn(bundle);
348             osgiBundlePlugin = new OsgiBundlePlugin(osgiContainerManager, PLUGIN_KEY, pluginArtifact);
349             osgiBundlePlugin.install();
350             // We know, and tested above, that getClassLoader only works once installed, but in fact the previous call with have
351             // thrown if we don't perform a reference install, since our mock can't respond.
352             osgiBundlePlugin.getClassLoader();
353         }
354     }
355 
356     public static class Legacy extends TestOsgiBundlePlugin
357     {
358         @Override
359         protected OsgiBundlePlugin getOsgiBundlePlugin()
360         {
361             //noinspection deprecation
362             return new OsgiBundlePlugin(bundle, PLUGIN_KEY, pluginArtifact);
363         }
364     }
365 }