/** * This file is part of Graylog. * * Graylog is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Graylog is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Graylog. If not, see <http://www.gnu.org/licenses/>. */ package org.graylog2.cluster; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.eventbus.Subscribe; import com.lordofthejars.nosqlunit.annotation.UsingDataSet; import com.lordofthejars.nosqlunit.core.LoadStrategyEnum; import com.lordofthejars.nosqlunit.mongodb.InMemoryMongoDb; import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DBCollection; import com.mongodb.DBObject; import com.mongodb.WriteConcern; import org.graylog2.bindings.providers.MongoJackObjectMapperProvider; import org.graylog2.database.MongoConnection; import org.graylog2.database.MongoConnectionRule; import org.graylog2.events.ClusterEventBus; import org.graylog2.plugin.cluster.ClusterConfigService; import org.graylog2.plugin.system.NodeId; import org.graylog2.shared.bindings.providers.ObjectMapperProvider; import org.graylog2.shared.plugins.ChainingClassLoader; import org.joda.time.DateTime; import org.joda.time.DateTimeUtils; import org.joda.time.DateTimeZone; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.Collections; import java.util.Map; import static com.lordofthejars.nosqlunit.mongodb.InMemoryMongoDb.InMemoryMongoRuleBuilder.newInMemoryMongoDbRule; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; public class ClusterConfigServiceImplTest { @ClassRule public static final InMemoryMongoDb IN_MEMORY_MONGO_DB = newInMemoryMongoDbRule().build(); private static final DateTime TIME = new DateTime(2015, 4, 1, 0, 0, DateTimeZone.UTC); private static final String COLLECTION_NAME = ClusterConfigServiceImpl.COLLECTION_NAME; @Rule public MongoConnectionRule mongoRule = MongoConnectionRule.build("test"); @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); private final ObjectMapper objectMapper = new ObjectMapperProvider().get(); @Mock private NodeId nodeId; @Spy private ClusterEventBus clusterEventBus; private MongoConnection mongoConnection; private ClusterConfigService clusterConfigService; @Before public void setUpService() throws Exception { DateTimeUtils.setCurrentMillisFixed(TIME.getMillis()); this.mongoConnection = mongoRule.getMongoConnection(); MongoJackObjectMapperProvider provider = new MongoJackObjectMapperProvider(objectMapper); when(nodeId.toString()).thenReturn("ID"); this.clusterConfigService = new ClusterConfigServiceImpl( provider, mongoRule.getMongoConnection(), nodeId, new ChainingClassLoader(getClass().getClassLoader()), clusterEventBus ); } @After public void tearDown() { DateTimeUtils.setCurrentMillisSystem(); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void getReturnsExistingConfig() throws Exception { DBObject dbObject = new BasicDBObjectBuilder() .add("type", CustomConfig.class.getCanonicalName()) .add("payload", Collections.singletonMap("text", "TEST")) .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get(); final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(dbObject); assertThat(collection.count()).isEqualTo(1L); CustomConfig customConfig = clusterConfigService.get(CustomConfig.class); assertThat(customConfig.text).isEqualTo("TEST"); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void getReturnsNullOnNonExistingConfig() throws Exception { final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); assertThat(collection.count()).isEqualTo(0L); assertThat(clusterConfigService.get(CustomConfig.class)).isNull(); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void getReturnsNullOnInvalidPayload() throws Exception { DBObject dbObject = new BasicDBObjectBuilder() .add("type", CustomConfig.class.getCanonicalName()) .add("payload", "wrong payload") .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get(); final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(dbObject); assertThat(collection.count()).isEqualTo(1L); assertThat(clusterConfigService.get(CustomConfig.class)).isNull(); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void getWithKeyReturnsExistingConfig() throws Exception { DBObject dbObject = new BasicDBObjectBuilder() .add("type", "foo") .add("payload", Collections.singletonMap("text", "TEST")) .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get(); final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(dbObject); assertThat(collection.count()).isEqualTo(1L); CustomConfig customConfig = clusterConfigService.get("foo", CustomConfig.class); assertThat(customConfig).isInstanceOf(CustomConfig.class); assertThat(customConfig.text).isEqualTo("TEST"); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void getWithKeyReturnsNullOnNonExistingConfig() throws Exception { final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); assertThat(collection.count()).isEqualTo(0L); assertThat(clusterConfigService.get("foo", CustomConfig.class)).isNull(); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void getOrDefaultReturnsExistingConfig() throws Exception { DBObject dbObject = new BasicDBObjectBuilder() .add("type", CustomConfig.class.getCanonicalName()) .add("payload", Collections.singletonMap("text", "TEST")) .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get(); final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(dbObject); assertThat(collection.count()).isEqualTo(1L); CustomConfig defaultValue = new CustomConfig(); defaultValue.text = "DEFAULT"; CustomConfig customConfig = clusterConfigService.getOrDefault(CustomConfig.class, defaultValue); assertThat(customConfig.text).isEqualTo("TEST"); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void getOrDefaultReturnsDefaultValueOnNonExistingConfig() throws Exception { final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); assertThat(collection.count()).isEqualTo(0L); CustomConfig defaultValue = new CustomConfig(); defaultValue.text = "DEFAULT"; assertThat(clusterConfigService.getOrDefault(CustomConfig.class, defaultValue)).isSameAs(defaultValue); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void getOrDefaultReturnsDefaultValueOnInvalidPayload() throws Exception { DBObject dbObject = new BasicDBObjectBuilder() .add("type", CustomConfig.class.getCanonicalName()) .add("payload", "wrong payload") .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get(); final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(dbObject); assertThat(collection.count()).isEqualTo(1L); CustomConfig defaultValue = new CustomConfig(); defaultValue.text = "DEFAULT"; assertThat(clusterConfigService.getOrDefault(CustomConfig.class, defaultValue)).isSameAs(defaultValue); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void writeIgnoresNull() throws Exception { final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); assertThat(collection.count()).isEqualTo(0L); clusterConfigService.write(null); assertThat(collection.count()).isEqualTo(0L); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void writePersistsClusterConfig() throws Exception { CustomConfig customConfig = new CustomConfig(); customConfig.text = "TEST"; final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); assertThat(collection.count()).isEqualTo(0L); clusterConfigService.write(customConfig); assertThat(collection.count()).isEqualTo(1L); DBObject dbObject = collection.findOne(); assertThat((String) dbObject.get("type")).isEqualTo(CustomConfig.class.getCanonicalName()); assertThat((String) dbObject.get("last_updated_by")).isEqualTo("ID"); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void writeUpdatesExistingClusterConfig() throws Exception { CustomConfig customConfig = new CustomConfig(); customConfig.text = "TEST"; DBObject seedObject = new BasicDBObjectBuilder() .add("type", CustomConfig.class.getCanonicalName()) .add("payload", Collections.singletonMap("text", "ORIGINAL")) .add("last_updated", TIME.toString()) .add("last_updated_by", "NOT ID") .get(); final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(seedObject); assertThat(collection.count()).isEqualTo(1L); clusterConfigService.write(customConfig); assertThat(collection.count()).isEqualTo(1L); DBObject dbObject = collection.findOne(); assertThat((String) dbObject.get("type")).isEqualTo(CustomConfig.class.getCanonicalName()); assertThat((String) dbObject.get("last_updated_by")).isEqualTo("ID"); @SuppressWarnings("unchecked") Map<String, Object> payload = (Map<String, Object>) dbObject.get("payload"); assertThat(payload).containsEntry("text", "TEST"); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void writePostsClusterConfigChangedEvent() throws Exception { CustomConfig customConfig = new CustomConfig(); customConfig.text = "TEST"; final ClusterConfigChangedEventHandler eventHandler = new ClusterConfigChangedEventHandler(); clusterEventBus.registerClusterEventSubscriber(eventHandler); final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); assertThat(collection.count()).isEqualTo(0L); clusterConfigService.write(customConfig); assertThat(collection.count()).isEqualTo(1L); assertThat(eventHandler.event).isNotNull(); assertThat(eventHandler.event.nodeId()).isEqualTo("ID"); assertThat(eventHandler.event.type()).isEqualTo(CustomConfig.class.getCanonicalName()); clusterEventBus.unregister(eventHandler); } @Test public void prepareCollectionCreatesIndexesOnExistingCollection() throws Exception { DBCollection original = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); original.dropIndexes(); assertThat(original.getName()).isEqualTo(COLLECTION_NAME); assertThat(original.getIndexInfo()).hasSize(1); DBCollection collection = ClusterConfigServiceImpl.prepareCollection(mongoConnection); assertThat(collection.getName()).isEqualTo(COLLECTION_NAME); assertThat(collection.getIndexInfo()).hasSize(2); assertThat(collection.getWriteConcern()).isEqualTo(WriteConcern.JOURNALED); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void prepareCollectionCreatesCollectionIfItDoesNotExist() throws Exception { assertThat(mongoConnection.getDatabase().collectionExists(COLLECTION_NAME)).isFalse(); DBCollection collection = ClusterConfigServiceImpl.prepareCollection(mongoConnection); assertThat(collection.getName()).isEqualTo(COLLECTION_NAME); assertThat(collection.getIndexInfo()).hasSize(2); assertThat(collection.getWriteConcern()).isEqualTo(WriteConcern.JOURNALED); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void removeDoesNothingIfConfigDoesNotExist() throws Exception { final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); assertThat(collection.count()).isEqualTo(0L); assertThat(clusterConfigService.remove(CustomConfig.class)).isEqualTo(0); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void removeSuccessfullyRemovesConfig() throws Exception { DBObject dbObject = new BasicDBObjectBuilder() .add("type", CustomConfig.class.getCanonicalName()) .add("payload", Collections.singletonMap("text", "TEST")) .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get(); final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(dbObject); assertThat(collection.count()).isEqualTo(1L); assertThat(clusterConfigService.remove(CustomConfig.class)).isEqualTo(1); assertThat(collection.count()).isEqualTo(0L); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void listReturnsAllClasses() throws Exception { final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(new BasicDBObjectBuilder() .add("type", CustomConfig.class.getCanonicalName()) .add("payload", Collections.singletonMap("text", "TEST")) .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get()); collection.save(new BasicDBObjectBuilder() .add("type", AnotherCustomConfig.class.getCanonicalName()) .add("payload", Collections.singletonMap("text", "TEST")) .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get()); assertThat(collection.count()).isEqualTo(2L); assertThat(clusterConfigService.list()) .hasSize(2) .containsOnly(CustomConfig.class, AnotherCustomConfig.class); } @Test @UsingDataSet(loadStrategy = LoadStrategyEnum.DELETE_ALL) public void listIgnoresInvalidClasses() throws Exception { final DBCollection collection = mongoConnection.getDatabase().getCollection(COLLECTION_NAME); collection.save(new BasicDBObjectBuilder() .add("type", CustomConfig.class.getCanonicalName()) .add("payload", Collections.singletonMap("text", "TEST")) .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get()); collection.save(new BasicDBObjectBuilder() .add("type", "invalid.ClassName") .add("payload", Collections.emptyMap()) .add("last_updated", TIME.toString()) .add("last_updated_by", "ID") .get()); assertThat(collection.count()).isEqualTo(2L); assertThat(clusterConfigService.list()) .hasSize(1) .containsOnly(CustomConfig.class); } public static class ClusterConfigChangedEventHandler { public volatile ClusterConfigChangedEvent event; @Subscribe public void handleSimpleEvent(ClusterConfigChangedEvent event) { this.event = ClusterConfigChangedEvent.create( event.date(), event.nodeId(), event.type() ); } } }