Skip to content

Commit

Permalink
openhab#16308 openhab#16310 Upgraded MongoDB driver, added initial un…
Browse files Browse the repository at this point in the history
…it tests

Signed-off-by: René Ulbricht <rene_ulbricht@outlook.com>
  • Loading branch information
ulbi committed Jan 28, 2024
1 parent 1ad86ea commit 8aa0071
Show file tree
Hide file tree
Showing 7 changed files with 1,127 additions and 29 deletions.
25 changes: 24 additions & 1 deletion bundles/org.openhab.persistence.mongodb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,29 @@
<version>1.44.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mongodb</artifactId>
<version>1.19.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;

import org.bson.Document;
import org.bson.types.ObjectId;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.params.provider.Arguments;
import org.mockito.Mockito;
import org.openhab.core.items.GenericItem;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.items.*;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.types.*;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.types.State;
import org.osgi.framework.BundleContext;
import org.slf4j.LoggerFactory;

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;

/**
* This class provides helper methods to create test items.
*
* @author René Ulbricht - Initial contribution
*/
public class DataCreationHelper {

private static final String FIELD_ID = "_id";
private static final String FIELD_ITEM = "item";
private static final String FIELD_REALNAME = "realName";
private static final String FIELD_TIMESTAMP = "timestamp";
private static final String FIELD_VALUE = "value";

/**
* Creates a NumberItem with a given name and value.
*
* @param name The name of the NumberItem.
* @param value The value of the NumberItem.
* @return The created NumberItem.
*/
public static NumberItem createNumberItem(String name, Number value) {
return createItem(NumberItem.class, name, new DecimalType(value));
}

/**
* Creates a StringItem with a given name and value.
*
* @param name The name of the StringItem.
* @param value The value of the StringItem.
* @return The created StringItem.
*/
public static StringItem createStringItem(String name, String value) {
return createItem(StringItem.class, name, new StringType(value));
}

/**
* Creates an instance of a specific GenericItem subclass and sets its state.
*
* @param <T> The type of the item to create. This must be a subclass of GenericItem.
* @param <S> The type of the state to set. This must be a subclass of State.
* @param itemType The Class object representing the type of the item to create.
* @param name The name to give to the new item.
* @param state The state to set on the new item.
* @return The newly created item.
* @throws RuntimeException if an error occurs while creating the item or setting its state.
*/
public static <T extends GenericItem, S extends State> T createItem(Class<T> itemType, String name, S state) {
try {
T item = itemType.getDeclaredConstructor(String.class).newInstance(name);
item.setState(state);
return item;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

/**
* Provides a stream of arguments for parameterized tests. Each argument is an instance of a specific
* GenericItem subclass with a set state.
*
* @return A stream of arguments for parameterized tests.
*/
public static Stream<Arguments> provideOpenhabItemTypes() {
return Stream.of(
Arguments.of(
DataCreationHelper.createItem(StringItem.class, "StringItem", new StringType("StringValue"))),
Arguments.of(DataCreationHelper.createItem(NumberItem.class, "NumberItem", new DecimalType(123.45))),
Arguments.of(DataCreationHelper.createItem(DimmerItem.class, "DimmerItem", new PercentType(50))),
Arguments.of(DataCreationHelper.createItem(SwitchItem.class, "SwitchItemON", OnOffType.ON)),
Arguments.of(DataCreationHelper.createItem(SwitchItem.class, "SwitchItemOFF", OnOffType.OFF)),
Arguments.of(DataCreationHelper.createItem(ContactItem.class, "ContactItemOPEN", OpenClosedType.OPEN)),
Arguments.of(
DataCreationHelper.createItem(ContactItem.class, "ContactItemCLOSED", OpenClosedType.CLOSED)),
Arguments.of(DataCreationHelper.createItem(RollershutterItem.class, "RollershutterItem",
new PercentType(30))),
Arguments.of(DataCreationHelper.createItem(DateTimeItem.class, "DateTimeItem",
new DateTimeType(ZonedDateTime.now()))),
// Arguments.of(DataCreationHelper.createItem(ColorItem.class, "ColorItem", new
// HSBType("180,100,100"))),
Arguments.of(
DataCreationHelper.createItem(LocationItem.class, "LocationItem", new PointType("51.0,0.0"))),
Arguments.of(DataCreationHelper.createItem(PlayerItem.class, "PlayerItem", PlayPauseType.PLAY)),
Arguments.of(
DataCreationHelper.createItem(PlayerItem.class, "PlayerItem", RewindFastforwardType.REWIND)));
}

/**
* Provides a stream of arguments to be used for parameterized tests.
*
* Each argument is a DatabaseTestContainer instance. Some instances use a MemoryBackend,
* while others use a MongoDBContainer with a specific MongoDB version.
*
* @return A stream of Arguments, each containing a DatabaseTestContainer instance.
*/
public static Stream<Arguments> provideDatabaseBackends() {
// Create a stream of Arguments
return Stream.of(
// Create a DatabaseTestContainer with a MemoryBackend
Arguments.of(new DatabaseTestContainer(new MemoryBackend())),
// Create DatabaseTestContainers with MongoDBContainers of specific versions
Arguments.of(new DatabaseTestContainer("mongo:3.6")),
Arguments.of(new DatabaseTestContainer("mongo:4.4")),
Arguments.of(new DatabaseTestContainer("mongo:5.0")),
Arguments.of(new DatabaseTestContainer("mongo:6.0")));
}

/**
* Creates a Document for a given item name, value, and timestamp.
*
* @param itemName The name of the item.
* @param value The value of the item.
* @param timestamp The timestamp of the item.
* @return The created Document.
*/
public static Document createDocument(String itemName, double value, LocalDate timestamp) {
Document obj = new Document();
obj.put(FIELD_ID, new ObjectId());
obj.put(FIELD_ITEM, itemName);
obj.put(FIELD_REALNAME, itemName);
obj.put(FIELD_TIMESTAMP, timestamp);
obj.put(FIELD_VALUE, value);
return obj;
}

/**
* Creates a FilterCriteria for a given item name.
*
* @param itemName The name of the item.
* @return The created FilterCriteria.
*/
public static FilterCriteria createFilterCriteria(String itemName) {
return createFilterCriteria(itemName, null, null);
}

/**
* Creates a FilterCriteria for a given item name, begin date, and end date.
*
* @param itemName The name of the item.
* @param beginDate The begin date of the FilterCriteria.
* @param endDate The end date of the FilterCriteria.
* @return The created FilterCriteria.
*/
public static FilterCriteria createFilterCriteria(String itemName, ZonedDateTime beginDate, ZonedDateTime endDate) {
FilterCriteria filter = new FilterCriteria();
filter.setItemName(itemName);
filter.setPageSize(10);
filter.setPageNumber(0);
filter.setOrdering(FilterCriteria.Ordering.ASCENDING);
filter.setBeginDate(beginDate);
filter.setEndDate(endDate);
return filter;
}

/**
* Sets up a MongoDB instance for testing.
*
* @param collectionName The name of the MongoDB collection to be used for testing.
* @param dbContainer The container running the MongoDB instance.
* @return A SetupResult object containing the MongoDBPersistenceService, the database, the bundle context, the
* configuration map, the item registry, and the database name.
*/
public static SetupResult setupMongoDB(String collectionName, DatabaseTestContainer dbContainer) {
// Start the database container
dbContainer.start();

// Mock the ItemRegistry and BundleContext
ItemRegistry itemRegistry = Mockito.mock(ItemRegistry.class);
BundleContext bundleContext = Mockito.mock(BundleContext.class);

// When getService is called on the bundleContext, return the mocked itemRegistry
when(bundleContext.getService(any())).thenReturn(itemRegistry);

// Create a new MongoDBPersistenceService instance
MongoDBPersistenceService service = new MongoDBPersistenceService(itemRegistry);

// Create a configuration map for the MongoDBPersistenceService
Map<String, Object> config = new HashMap<>();
config.put("url", dbContainer.getConnectionString());
String dbname = UUID.randomUUID().toString();
config.put("database", dbname);
if (collectionName != null) {
config.put("collection", collectionName);
}

// Create a MongoClient connected to the mock server
MongoClient mongoClient = MongoClients.create(dbContainer.getConnectionString());

// Create a database and collection
MongoDatabase database = mongoClient.getDatabase(dbname);

// Setup logger to capture log events
Logger logger = (Logger) LoggerFactory.getLogger(MongoDBPersistenceService.class);
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.start();
logger.addAppender(listAppender);
logger.setLevel(Level.WARN);

// Return a SetupResult object containing the service, database, bundle context, config, item registry, and
// database name
return new SetupResult(service, database, bundleContext, config, itemRegistry, dbname);
}

/**
* Sets up a logger to capture log events.
*
* @param loggerClass The class that the logger is for.
* @param level The level of the logger.
* @return The list appender attached to the logger.
*/
public static ListAppender<ILoggingEvent> setupLogger(Class<?> loggerClass, Level level) {
Logger logger = (Logger) LoggerFactory.getLogger(loggerClass);
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.start();
logger.addAppender(listAppender);
logger.setLevel(level); // Set log level
return listAppender;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.persistence.mongodb.internal;

import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;

import org.testcontainers.containers.MongoDBContainer;

import de.bwaldvogel.mongo.MongoServer;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;

/**
* This class provides a container for MongoDB for testing purposes.
* It uses the Testcontainers library to manage the MongoDB container.
* It also provides an in-memory MongoDB server for testing.
*
* @author René Ulbricht - Initial contribution
*/
public class DatabaseTestContainer {
// A map to store MongoDBContainer instances for different MongoDB versions.
private static final Map<String, MongoDBContainer> mongoDBContainers = new HashMap<>();

// The MongoDBContainer instance for this DatabaseTestContainer.
private MongoDBContainer mongoDBContainer;

// The MongoServer instance for this DatabaseTestContainer.
private MongoServer server;

// The MemoryBackend instance for this DatabaseTestContainer.
private MemoryBackend memoryBackend;

// The InetSocketAddress instance for this DatabaseTestContainer.
private InetSocketAddress serverAddress;

/**
* Creates a new DatabaseTestContainer for a given MongoDB version.
* If a MongoDBContainer for the given version already exists, it is reused.
*
* @param mongoDBVersion The version of MongoDB to use.
*/
public DatabaseTestContainer(String mongoDBVersion) {
mongoDBContainer = mongoDBContainers.computeIfAbsent(mongoDBVersion, MongoDBContainer::new);
}

/**
* Creates a new DatabaseTestContainer for an in-memory MongoDB server.
*/
public DatabaseTestContainer(MemoryBackend memoryBackend) {
server = new MongoServer(memoryBackend);
serverAddress = server.bind();
}

/**
* Starts the MongoDB container or the in-memory MongoDB server.
*/
public void start() {
if (mongoDBContainer != null && !mongoDBContainer.isRunning()) {
mongoDBContainer.start();
}
}

/**
* Don't do anything.
*/
public void stop() {
}

/**
* Returns the connection string for connecting to the MongoDB container or the in-memory MongoDB server.
*
* @return The connection string.
*/
public String getConnectionString() {
if (mongoDBContainer != null) {
return mongoDBContainer.getConnectionString();
} else if (server != null) {
return String.format("mongodb://%s:%s", serverAddress.getHostName(), serverAddress.getPort());
} else {
return null;
}
}
}

0 comments on commit 8aa0071

Please sign in to comment.