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