/* * Copyright 2014-2015 Groupon, Inc * Copyright 2014-2015 The Billing Project, LLC * * The Billing Project licenses this file to you under the Apache License, version 2.0 * (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package org.killbill.billing.beatrix.integration; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import javax.annotation.Nullable; import javax.inject.Inject; import org.killbill.billing.DBTestingHelper; import org.killbill.billing.api.TestApiListener.NextEvent; import org.killbill.billing.notification.plugin.api.BroadcastMetadata; import org.killbill.billing.notification.plugin.api.ExtBusEvent; import org.killbill.billing.notification.plugin.api.ExtBusEventType; import org.killbill.billing.osgi.BundleRegistry; import org.killbill.billing.osgi.BundleWithConfig; import org.killbill.billing.osgi.FileInstall; import org.killbill.billing.osgi.PureOSGIBundleFinder; import org.killbill.billing.osgi.api.PluginInfo; import org.killbill.billing.osgi.api.PluginStateChange; import org.killbill.billing.osgi.api.PluginsInfoApi; import org.killbill.billing.osgi.api.config.PluginConfig; import org.killbill.billing.osgi.api.config.PluginConfigServiceApi; import org.killbill.billing.osgi.api.config.PluginLanguage; import org.killbill.billing.osgi.api.config.PluginType; import org.killbill.billing.osgi.config.OSGIConfig; import org.killbill.billing.osgi.pluginconf.PluginConfigException; import org.killbill.billing.osgi.pluginconf.PluginFinder; import org.killbill.billing.platform.api.KillbillConfigSource; import org.killbill.billing.util.jackson.ObjectMapper; import org.killbill.billing.util.nodes.KillbillNodesApi; import org.killbill.billing.util.nodes.NodeCommand; import org.killbill.billing.util.nodes.NodeCommandMetadata; import org.killbill.billing.util.nodes.NodeCommandProperty; import org.killbill.billing.util.nodes.NodeInfo; import org.killbill.billing.util.nodes.NodeInfoMapper; import org.killbill.billing.util.nodes.PluginNodeCommandMetadata; import org.killbill.billing.util.nodes.SystemNodeCommandType; import org.mockito.Mockito; import org.osgi.framework.Bundle; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.joda.JodaModule; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.eventbus.Subscribe; import com.google.inject.Binder; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Stage; import com.google.inject.util.Modules; import static org.awaitility.Awaitility.await; import static java.util.concurrent.TimeUnit.SECONDS; public class TestWithFakeKPMPlugin extends TestIntegrationBase { private static final String NEW_PLUGIN_NAME = "Foo"; private static final String NEW_PLUGIN_VERSION = "2.5.7"; @Inject private PluginsInfoApi pluginsInfoApi; @Inject private PluginFinder pluginFinder; @Override protected KillbillConfigSource getConfigSource() { ImmutableMap additionalProperties = new ImmutableMap.Builder() .put("org.killbill.billing.util.broadcast.rate", "100ms") .build(); return getConfigSource("/beatrix.properties", additionalProperties); } public class FakeKPMPlugin { private final NodeInfoMapper nodeInfoMapper; private final ObjectMapper objectMapper; FakeKPMPlugin() { this.nodeInfoMapper = new NodeInfoMapper(); this.objectMapper = new ObjectMapper(); objectMapper.registerModule(new JodaModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); } @Subscribe public void handleExternalEvents(final ExtBusEvent extBusEvent) { if (extBusEvent.getEventType().equals(ExtBusEventType.BROADCAST_SERVICE)) { final String metadata = extBusEvent.getMetaData(); try { final BroadcastMetadata broadcastMetadata = objectMapper.readValue(metadata, BroadcastMetadata.class); final PluginNodeCommandMetadata nodeCommandMetadata = (PluginNodeCommandMetadata) nodeInfoMapper.deserializeNodeCommand(broadcastMetadata.getEventJson(), broadcastMetadata.getCommandType()); ((FakePluginFinder) pluginFinder).addPlugin(createPluginConfig(nodeCommandMetadata)); pluginsInfoApi.notifyOfStateChanged(PluginStateChange.NEW_VERSION, nodeCommandMetadata.getPluginKey(), nodeCommandMetadata.getPluginName(), nodeCommandMetadata.getPluginVersion(), PluginLanguage.JAVA); } catch (IOException e) { throw new RuntimeException(e); } } } } private PluginConfig createPluginConfig(final PluginNodeCommandMetadata nodeCommandMetadata) { return new PluginConfig() { @Override public int compareTo(final PluginConfig o) { return 0; } @Override public String getPluginKey() { return nodeCommandMetadata.getPluginKey(); } @Override public String getPluginName() { return nodeCommandMetadata.getPluginName(); } @Override public PluginType getPluginType() { return PluginType.NOTIFICATION; } @Override public String getVersion() { return nodeCommandMetadata.getPluginVersion(); } @Override public String getPluginVersionnedName() { return getPluginName() + "-" + getVersion(); } @Override public File getPluginVersionRoot() { return null; } @Override public PluginLanguage getPluginLanguage() { return PluginLanguage.JAVA; } @Override public boolean isSelectedForStart() { return true; } @Override public boolean isDisabled() { return false; } }; } // We override the BundleRegistry to bypass the bundle installation and yet return our new bundle as being installed. private static class FakePluginFinder extends PluginFinder { @Inject public FakePluginFinder(final OSGIConfig osgiConfig) { super(osgiConfig); } public void reloadPlugins() throws PluginConfigException, IOException { } public void addPlugin(final PluginConfig newPlugin) { final Map<String, LinkedList<PluginConfig>> allPluginField = getAllPluginField(); allPluginField.clear(); if (allPluginField.get(newPlugin.getPluginName()) == null) { allPluginField.put(newPlugin.getPluginName(), new LinkedList<PluginConfig>()); } allPluginField.get(newPlugin.getPluginName()).add(newPlugin); } private Map<String, LinkedList<PluginConfig>> getAllPluginField() { try { final Field f = PluginFinder.class.getDeclaredField("allPlugins"); f.setAccessible(true); return (Map<String, LinkedList<PluginConfig>>) f.get(this); } catch (NoSuchFieldException e) { throw new RuntimeException("Failed to retrieve private field allPlugins from PluginFinder class ", e); } catch (IllegalAccessException e) { throw new RuntimeException("Failed to retrieve private field allPlugins from PluginFinder class ", e); } } } private static class FakeBundleRegistry extends BundleRegistry { private final List<BundleWithMetadata> bundles; @Inject public FakeBundleRegistry() { super(null); bundles = new ArrayList<BundleWithMetadata>(); } public void installNewBundle(final String pluginName, @Nullable final String version, final PluginLanguage pluginLanguage) { final Bundle bundle = Mockito.mock(Bundle.class); Mockito.when(bundle.getSymbolicName()).thenReturn(pluginName); final BundleWithConfig config = new BundleWithConfig(bundle, new PluginConfig() { @Override public int compareTo(final PluginConfig o) { return 0; } @Override public String getPluginKey() { return null; } @Override public String getPluginName() { return pluginName; } @Override public PluginType getPluginType() { return PluginType.NOTIFICATION; } @Override public String getVersion() { return version; } @Override public String getPluginVersionnedName() { return null; } @Override public File getPluginVersionRoot() { return null; } @Override public PluginLanguage getPluginLanguage() { return pluginLanguage; } @Override public boolean isSelectedForStart() { return true; } @Override public boolean isDisabled() { return false; } }); bundles.add(new BundleWithMetadata(config)); } public BundleWithMetadata getBundle(final String pluginName) { return Iterables.tryFind(bundles, new Predicate<BundleWithMetadata>() { @Override public boolean apply(@Nullable final BundleWithMetadata input) { return input.getPluginName().equals(pluginName); } }).orNull(); } public Collection<BundleWithMetadata> getBundles() { return bundles; } } public static class OverrideModuleForOSGI implements Module { @Override public void configure(final Binder binder) { binder.bind(BundleRegistry.class).to(FakeBundleRegistry.class).asEagerSingleton(); binder.bind(PluginFinder.class).to(FakePluginFinder.class).asEagerSingleton(); } } @BeforeClass(groups = "slow") public void beforeClass() throws Exception { final Injector g = Guice.createInjector(Stage.PRODUCTION, Modules.override(new BeatrixIntegrationModule(configSource)).with(new OverrideModuleForOSGI())); g.injectMembers(this); } @BeforeClass(groups = "slow") public void beforeMethod() throws Exception { try { DBTestingHelper.get().getInstance().cleanupAllTables(); } catch (final Exception ignored) { } log.debug("RESET TEST FRAMEWORK"); clock.resetDeltaFromReality(); busHandler.reset(); lifecycle.fireStartupSequencePriorEventRegistration(); busService.getBus().register(busHandler); externalBus.register(new FakeKPMPlugin()); lifecycle.fireStartupSequencePostEventRegistration(); // Make sure we start with a clean state assertListenerStatus(); } @Test(groups = "slow") public void testPluginInstallMechanism() throws Exception { final NodeCommand nodeCommand = new NodeCommand() { @Override public boolean isSystemCommandType() { return true; } @Override public String getNodeCommandType() { return SystemNodeCommandType.INSTALL_PLUGIN.name(); } @Override public NodeCommandMetadata getNodeCommandMetadata() { return new PluginNodeCommandMetadata(NEW_PLUGIN_NAME, NEW_PLUGIN_NAME, NEW_PLUGIN_VERSION, ImmutableList.<NodeCommandProperty>of()); } }; busHandler.pushExpectedEvent(NextEvent.BROADCAST_SERVICE); nodesApi.triggerNodeCommand(nodeCommand, false); assertListenerStatus(); // Exit condition is based on the new config being updated on disk await().atMost(3, SECONDS).until(new Callable<Boolean>() { @Override public Boolean call() throws Exception { final Iterable<NodeInfo> rawNodeInfos = nodesApi.getNodesInfo(); final List<NodeInfo> nodeInfos = ImmutableList.<NodeInfo>copyOf(rawNodeInfos); Assert.assertEquals(nodeInfos.size(), 1); final NodeInfo nodeInfo = nodeInfos.get(0); final Iterable<PluginInfo> rawPluginInfos = nodeInfo.getPluginInfo(); final List<PluginInfo> pluginsInfo = ImmutableList.copyOf(rawPluginInfos); if (pluginsInfo.size() == 1) { final PluginInfo pluginInfo = pluginsInfo.get(0); Assert.assertEquals(pluginInfo.getPluginName(), NEW_PLUGIN_NAME); Assert.assertEquals(pluginInfo.getVersion(), NEW_PLUGIN_VERSION); } return pluginsInfo.size() == 1; } }); } }