diff --git a/.gitignore b/.gitignore index 24affe0..7d81f52 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ################################################################################################### config.properties +products-config.json +persistence.db ################################################################################################### ## Visual Studio Code ############################################################################# diff --git a/CHANGELOG.md b/CHANGELOG.md index d62761a..71620ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,14 @@ ### Added - `EbayItemDataSource` (#37) +- Product data persistence via SQLite integration (#53) - Mutation testing capabilities via PITest (#66) - `Product` class - Mapping between Product and its corresponding DTO - Dependencies: Lombok for easier notation, JUnit 5 Params for repetitive tests -- Product Validation -- Product Retrieval -- Unit Tests +- Product validation, retrieval and persistence writing +- Product catalogue retrieval and persistence writing +- More unit tests ### Changed diff --git a/Script/SonarQube_Local.sh b/Script/SonarQube_Local.sh index 18b0b37..14d2602 100644 --- a/Script/SonarQube_Local.sh +++ b/Script/SonarQube_Local.sh @@ -2,4 +2,6 @@ mvn clean verify sonar:sonar -Pcoverage \ -Dsonar.projectKey=EasyDrop \ -Dsonar.projectName='EasyDrop' \ -Dsonar.host.url=http://localhost:9000 \ - -Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931 \ No newline at end of file + -Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931 + +start http://localhost:9000/dashboard?id=EasyDrop \ No newline at end of file diff --git a/config/demo.products-config.json b/config/demo.products-config.json new file mode 100644 index 0000000..2aa9058 --- /dev/null +++ b/config/demo.products-config.json @@ -0,0 +1,16 @@ +{ + "products": [ + { + "name": "Gigabyte GeForce RTX 3060", + "description": "Very epic GPU", + "identifiers": [ + { + "Amazon": "B096Y2TYKV" + }, + { + "eBay": "Gigabyte GeForce RTX 3060" + } + ] + } + ] +} diff --git a/pom.xml b/pom.xml index d45d187..58603a6 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,12 @@ logback-classic 1.4.7 + + + org.xerial + sqlite-jdbc + 3.42.0.0 + diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index 74b6aec..5441d96 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -1,13 +1,22 @@ package de.rwu.easydrop; +import java.util.List; + import javax.naming.ConfigurationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sqlite.SQLiteDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.SQLiteConnector; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.retriever.CatalogueRetriever; import de.rwu.easydrop.service.retriever.ProductRetriever; +import de.rwu.easydrop.service.writer.CatalogueWriter; import de.rwu.easydrop.util.Config; +import de.rwu.easydrop.util.ProductsConfig; /** * Kickoff point for the service. @@ -34,12 +43,20 @@ public final class Main { */ public static void main(final String[] args) throws ConfigurationException { Config config = Config.getInstance(); + ProductsConfig pConfig = ProductsConfig.getInstance(); DataSourceFactory dataSourceFactory = new DataSourceFactory(config); ProductRetriever retriever = new ProductRetriever(dataSourceFactory); + CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); + AbstractProductPersistence db = new SQLiteConnector(new SQLiteDataSource()); + CatalogueWriter catWriter = new CatalogueWriter(db); - String amznProduct = retriever.getProductFromAmazon("B096Y2TYKV").toString(); - LOGGER.info(amznProduct); - String ebayProduct = retriever.getProductFromEbay("Gigabyte GeForce RTX 3060").toString(); - LOGGER.info(ebayProduct); + catRetriever.loadCatalogues(); + List pCats = catRetriever.getProductCatalogues(); + catWriter.writeCatalogues(pCats); + + for (ProductCatalogue pCat : pCats) { + String pCatStr = pCat.toString(); + LOGGER.info(pCatStr); + } } } diff --git a/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java index ba79ab5..490466c 100644 --- a/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java +++ b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java @@ -2,6 +2,8 @@ package de.rwu.easydrop.api.client; import javax.naming.ConfigurationException; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.util.Config; /** @@ -15,6 +17,17 @@ public class DataSourceFactory { * The data source config. */ private Config config; + /** + * Persistence interface. + */ + private AbstractProductPersistence persistence = null; + + /** + * @param newPersistence the persistence to set + */ + public void setPersistence(final AbstractProductPersistence newPersistence) { + this.persistence = newPersistence; + } /** * @param newConfig the config to set @@ -52,4 +65,17 @@ public class DataSourceFactory { String apiKey = config.getProperty("EBAY_API_KEY"); return new EbayItemDataSource(apiUrl, apiKey); } + + /** + * Creates a persistence data source. + * + * @return ProductPersistenceInterface + */ + public AbstractProductPersistence createProductPersistenceDataSource() { + if (persistence == null) { + throw new PersistenceException("Persistence is not set"); + } + + return persistence; + } } diff --git a/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java b/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java new file mode 100644 index 0000000..6a302dc --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java @@ -0,0 +1,34 @@ +package de.rwu.easydrop.data.connector; + +import de.rwu.easydrop.api.client.AbstractDataSource; +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Allows connecting to a persistent product data store. + * + * @since 0.2.0 + */ +public abstract class AbstractProductPersistence extends AbstractDataSource { + /** + * Data origin. + */ + public static final String DATA_ORIGIN = "Persistence"; + + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + public abstract void saveProduct(ProductDTO dto); + + /** + * Gets a ProductDTO from persistence. + */ + @Override + public abstract ProductDTO getProductDTOById(String productId); + + /** + * Deletes all data from persistence. + */ + public abstract void clearData(); +} diff --git a/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java b/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java deleted file mode 100644 index ce6360a..0000000 --- a/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.data.connector; - -/** - * Allows connecting to a SQLite Database. - * - * TODO implement - */ -public class DatabaseConnector { - -} diff --git a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java new file mode 100644 index 0000000..6947c5d --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -0,0 +1,173 @@ +package de.rwu.easydrop.data.connector; + +import java.net.URL; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.sqlite.SQLiteDataSource; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.PersistenceException; + +/** + * Allows connecting to a SQLite Database. + * + * @since 0.2.0 + */ +public final class SQLiteConnector extends AbstractProductPersistence { + /** + * Data origin. + */ + private static final String DATA_ORIGIN = "SQLite"; + + /** + * SQLite Database. + */ + private SQLiteDataSource db; + + /** + * @param src the db to set + */ + public void setDb(final SQLiteDataSource src) { + this.db = src; + } + + /** + * Path to SQLite db file. + */ + private static final String PERSISTENCE_PATH = "jdbc:sqlite:persistence.db"; + + /** + * Creates instance. + * + * @param src SQLite Data Source + */ + public SQLiteConnector(final SQLiteDataSource src) { + db = src; + db.setUrl(PERSISTENCE_PATH); + initializeDatabase(); + } + + private void initializeDatabase() { + try { + // Create a new database connection + Connection connection = db.getConnection(); + + // Execute SQL statements to create tables + Statement statement = connection.createStatement(); + statement.execute( + "CREATE TABLE IF NOT EXISTS products (" + + "dataOrigin TEXT, " + + "productId TEXT, " + + "currentPrice REAL, " + + "merchant TEXT, " + + "deliveryPrice REAL, " + + "available INT, " + + "lastupdate TEXT, " + + "UNIQUE(productId, dataOrigin) ON CONFLICT REPLACE" + + ")"); + + // Close the statement and connection + statement.close(); + connection.close(); + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while initializing SQLite DB", e); + } + } + + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + public void saveProduct(final ProductDTO dto) { + String query = "INSERT INTO products (" + + "dataOrigin, productId, currentPrice, merchant, " + + "deliveryPrice, available, lastupdate" + + ") VALUES (" + + "?, ?, ?, ?, ?, ?, datetime('now', 'localtime')" + + ")"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + int index = 0; + + statement.setString(++index, dto.getDataOrigin()); + statement.setString(++index, dto.getProductId()); + statement.setDouble(++index, dto.getCurrentPrice()); + statement.setString(++index, dto.getMerchant()); + statement.setDouble(++index, dto.getDeliveryPrice()); + statement.setBoolean(++index, dto.isAvailable()); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while saving to SQLite", e); + } + } + + @Override + public ProductDTO getProductDTOById(final String productId) { + String query = "SELECT * FROM products WHERE productId = ?"; + ProductDTO dto = null; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + + statement.setString(1, productId); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + dto = new ProductDTO(resultSet.getString("productId"), + resultSet.getString("dataOrigin")); + dto.setCurrentPrice(resultSet.getDouble("currentPrice")); + dto.setMerchant(resultSet.getString("merchant")); + dto.setDeliveryPrice(resultSet.getDouble("deliveryPrice")); + dto.setAvailable(resultSet.getBoolean("available")); + } + } + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while reading from SQLite", e); + } + + return dto; + } + + /** + * Deletes all data from persistence. + */ + public void clearData() { + try (Connection connection = db.getConnection(); + Statement statement = connection.createStatement()) { + String query = "DELETE FROM products"; + statement.executeUpdate(query); + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while clearing the database", e); + } + } + + @Override + protected String getDataOrigin() { + return DATA_ORIGIN; + } + + @Override + protected String getApiKey() { + throw new UnsupportedOperationException( + this.getClass().getName() + " doesn't support getApiKey"); + } + + @Override + protected ProductDTO buildProductDTO(final ProductDTO product, final String json) { + throw new UnsupportedOperationException( + this.getClass().getName() + " doesn't support buildProductDTO"); + } + + @Override + protected URL createApiUrl(final String productIdentifier) { + throw new UnsupportedOperationException( + this.getClass().getName() + " doesn't support createApiUrl"); + } +} diff --git a/src/main/java/de/rwu/easydrop/data/connector/package-info.java b/src/main/java/de/rwu/easydrop/data/connector/package-info.java index f5b2dc7..77ea22e 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/package-info.java +++ b/src/main/java/de/rwu/easydrop/data/connector/package-info.java @@ -1,6 +1,6 @@ /** * Connectors for databases. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.data.connector; diff --git a/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java b/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java deleted file mode 100644 index f787da9..0000000 --- a/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.data.dao; - -/** - * Product data access object. - * - * TODO implement - */ -public class ProductDAO { - -} diff --git a/src/main/java/de/rwu/easydrop/data/dao/package-info.java b/src/main/java/de/rwu/easydrop/data/dao/package-info.java deleted file mode 100644 index 2affd23..0000000 --- a/src/main/java/de/rwu/easydrop/data/dao/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Data access objects for business objects created from persistence. - * - * TODO implement - */ -package de.rwu.easydrop.data.dao; diff --git a/src/main/java/de/rwu/easydrop/data/package-info.java b/src/main/java/de/rwu/easydrop/data/package-info.java index a161322..dfbf299 100644 --- a/src/main/java/de/rwu/easydrop/data/package-info.java +++ b/src/main/java/de/rwu/easydrop/data/package-info.java @@ -1,6 +1,6 @@ /** * Structure for business objects and persisting their info. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.data; diff --git a/src/main/java/de/rwu/easydrop/exception/PersistenceException.java b/src/main/java/de/rwu/easydrop/exception/PersistenceException.java new file mode 100644 index 0000000..0e32528 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/PersistenceException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies a problem with data persistence. + * + * @since 0.2.0 + */ +public class PersistenceException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public PersistenceException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public PersistenceException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java new file mode 100644 index 0000000..97f31dc --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java @@ -0,0 +1,79 @@ +package de.rwu.easydrop.model; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Data; + +/** + * Holds Product instances for one product from different data sources. + * + * @since 0.2.0 + */ +@Data +public class ProductCatalogue { + /** + * Product name. + */ + private String productName; + /** + * Product description. + */ + private String description; + /** + * Product collection. + */ + private List products; + + /** + * Creates new Product Catalogue. + * + * @param newProductName + * @param newDescription + */ + public ProductCatalogue(final String newProductName, final String newDescription) { + this.productName = newProductName; + this.description = newDescription; + this.products = new ArrayList<>(); + } + + /** + * Adds a product to the catalogue. + * + * @param product + */ + public void addProduct(final Product product) { + products.add(product); + } + + /** + * Removes a product from the catalogue. + * + * @param product + */ + public void removeProduct(final Product product) { + products.remove(product); + } + + /** + * Removes all products from the catalogue. + */ + public void clearProducts() { + products = new ArrayList<>(); + } + + /** + * Outputs the catalogue as a string. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Product Catalogue: ").append(productName).append("\n"); + sb.append("Description: ").append(description).append("\n"); + sb.append("Products:\n"); + for (Product product : products) { + sb.append(product.toString()).append("\n"); + } + return sb.toString(); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java index 1b37a11..123080a 100644 --- a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java +++ b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java @@ -1,16 +1,17 @@ package de.rwu.easydrop.service.mapping; import de.rwu.easydrop.api.dto.ProductDTO; + import de.rwu.easydrop.model.Product; /** - * Maps between Product, ProductDAO and ProductDTO. + * Maps between Product, ProductDTO and ProductDTO. * * @since 0.2.0 * * @see Product * @see ProductDTO - * @see ProductDAO + * @see ProductDTO */ public final class ProductMapper { @@ -41,4 +42,21 @@ public final class ProductMapper { return product; } + + /** + * Creates a ProductDTO object from a corresponding Product. + * + * @param product Product + * @return ProductDTO + */ + public static ProductDTO mapProductToDTO(final Product product) { + ProductDTO dto = new ProductDTO(product.getProductId(), product.getDataOrigin()); + + dto.setAvailable(product.isAvailable()); + dto.setCurrentPrice(product.getCurrentPrice()); + dto.setDeliveryPrice(product.getDeliveryPrice()); + dto.setMerchant(product.getMerchant()); + + return dto; + } } diff --git a/src/main/java/de/rwu/easydrop/service/package-info.java b/src/main/java/de/rwu/easydrop/service/package-info.java index ff59e0c..18ad0d7 100644 --- a/src/main/java/de/rwu/easydrop/service/package-info.java +++ b/src/main/java/de/rwu/easydrop/service/package-info.java @@ -1,6 +1,6 @@ /** * Packages for supporting business logic. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.service; diff --git a/src/main/java/de/rwu/easydrop/service/processing/package-info.java b/src/main/java/de/rwu/easydrop/service/processing/package-info.java index db7cb9e..c373207 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/package-info.java +++ b/src/main/java/de/rwu/easydrop/service/processing/package-info.java @@ -1,6 +1,6 @@ /** * Supports diverse business processes and enforces business rules. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.service.processing; diff --git a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java new file mode 100644 index 0000000..64bb8c0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java @@ -0,0 +1,78 @@ +package de.rwu.easydrop.service.retriever; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.ConfigurationException; + +import de.rwu.easydrop.exception.InvalidProductException; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.util.ProductsConfig; +import lombok.Data; + +/** + * Retrieves data for all products of multiple catalogues. + * + * @since 0.2.0 + */ +@Data +public class CatalogueRetriever { + /** + * User-configured products. + */ + private ProductsConfig productsConfig; + /** + * Product Retriever. + */ + private ProductRetriever productRetriever; + /** + * Product catalogue. + */ + private List productCatalogues; + + /** + * Creates a new instance. + * + * @param newProductsConfig + * @param newProductRetriever + */ + public CatalogueRetriever( + final ProductsConfig newProductsConfig, final ProductRetriever newProductRetriever) { + productRetriever = newProductRetriever; + productsConfig = newProductsConfig; + productCatalogues = new ArrayList<>(); + } + + /** + * Loads catalogues as configured by the user. + * + * @throws ConfigurationException + */ + public void loadCatalogues() throws ConfigurationException { + productsConfig.loadConfig(); + + for (ProductCatalogue pCat : productsConfig.getProductCatalogues()) { + ProductCatalogue newProductCatalogue = new ProductCatalogue( + pCat.getProductName(), pCat.getDescription()); + + for (Product product : pCat.getProducts()) { + Product newProduct = new Product(); + newProduct.setDataOrigin(product.getDataOrigin()); + newProduct.setProductId(product.getProductId()); + + if (newProduct.getDataOrigin().equals("Amazon")) { + newProduct = productRetriever.getProductFromAmazon(product.getProductId()); + } else if (newProduct.getDataOrigin().equals("eBay")) { + newProduct = productRetriever.getProductFromEbay(product.getProductId()); + } else { + throw new InvalidProductException("Product data origin is invalid"); + } + + newProductCatalogue.addProduct(newProduct); + } + + productCatalogues.add(newProductCatalogue); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java index bce5d10..da842c3 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -4,6 +4,7 @@ import de.rwu.easydrop.api.client.AmazonProductDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.validation.ProductValidator; @@ -64,4 +65,20 @@ public class ProductRetriever { return product; } + + /** + * Retrieves a product from persistence. + * + * @param productId + * @return Product from persistence + */ + public Product getProductFromPersistence(final String productId) { + AbstractProductPersistence src = dataSourceFactory.createProductPersistenceDataSource(); + + ProductDTO dto = src.getProductDTOById(productId); + Product product = ProductMapper.mapProductFromDTO(dto); + ProductValidator.validate(product); + + return product; + } } diff --git a/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java b/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java new file mode 100644 index 0000000..a1cf486 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java @@ -0,0 +1,47 @@ +package de.rwu.easydrop.service.writer; + +import java.util.List; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.mapping.ProductMapper; +import de.rwu.easydrop.service.validation.ProductValidator; + +/** + * Writes data for all products of multiple catalogues to persistence. + * + * @since 0.2.0 + */ +public final class CatalogueWriter { + /** + * Holds a persistence reference. + */ + private AbstractProductPersistence persistence; + + /** + * Creates new instance. + * + * @param newPersistence + */ + public CatalogueWriter(final AbstractProductPersistence newPersistence) { + persistence = newPersistence; + } + + /** + * Writes all products of specified catalogues to persistence. + * + * @param catalogues + */ + public void writeCatalogues(final List catalogues) { + for (ProductCatalogue pCat : catalogues) { + for (Product product : pCat.getProducts()) { + ProductValidator.validate(product); + ProductDTO dto = ProductMapper.mapProductToDTO(product); + + persistence.saveProduct(dto); + } + } + } +} diff --git a/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java b/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java new file mode 100644 index 0000000..d1be850 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java @@ -0,0 +1,38 @@ +package de.rwu.easydrop.service.writer; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.service.mapping.ProductMapper; +import de.rwu.easydrop.service.validation.ProductValidator; + +/** + * Wrapper for writing product info to persistence. + * + * @since 0.2.0 + */ +public class ProductWriter { + /** + * Persistence. + */ + private AbstractProductPersistence persistence; + + /** + * @param newPersistence the persistence to set + */ + public void setPersistence(final AbstractProductPersistence newPersistence) { + this.persistence = newPersistence; + } + + /** + * Validates and saves product to persistence. + * + * @param product + */ + public void writeProductToPersistence(final Product product) { + ProductValidator.validate(product); + ProductDTO dto = ProductMapper.mapProductToDTO(product); + + persistence.saveProduct(dto); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/writer/package-info.java b/src/main/java/de/rwu/easydrop/service/writer/package-info.java new file mode 100644 index 0000000..09c58f8 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/package-info.java @@ -0,0 +1,6 @@ +/** + * Writes Objects to a data store. + * + * @since 0.2.0 + */ +package de.rwu.easydrop.service.writer; diff --git a/src/main/java/de/rwu/easydrop/util/Config.java b/src/main/java/de/rwu/easydrop/util/Config.java index 32cce6a..e40b3d4 100644 --- a/src/main/java/de/rwu/easydrop/util/Config.java +++ b/src/main/java/de/rwu/easydrop/util/Config.java @@ -52,7 +52,6 @@ public final class Config { * Returns current config instance. * * @return Config instance - * @throws ConfigurationException */ public static Config getInstance() { if (instance == null) { diff --git a/src/main/java/de/rwu/easydrop/util/ProductsConfig.java b/src/main/java/de/rwu/easydrop/util/ProductsConfig.java new file mode 100644 index 0000000..b69777f --- /dev/null +++ b/src/main/java/de/rwu/easydrop/util/ProductsConfig.java @@ -0,0 +1,142 @@ +package de.rwu.easydrop.util; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import javax.naming.ConfigurationException; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.JsonPathException; + +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; + +/** + * Reads the user-specified catalogue of products. + * + * @since 0.2.0 + */ +public final class ProductsConfig { + /** + * Products config location. + */ + private String configLocation = "config/products-config.json"; + + /** + * @return the configLocation + */ + public String getConfigLocation() { + return configLocation; + } + + /** + * @param newConfigLocation the configLocation to set + */ + public void setConfigLocation(final String newConfigLocation) { + this.configLocation = newConfigLocation; + } + + /** + * Holds the configured products. + */ + private List productCatalogues; + + /** + * @return the product catalogues + */ + public List getProductCatalogues() { + return productCatalogues; + } + + /** + * Singleton instance. + */ + private static ProductsConfig instance = null; + + /** + * Private constructor to prevent external instantiation. + */ + private ProductsConfig() { + productCatalogues = new ArrayList<>(); + } + + /** + * Returns current products config instance. + * + * @return Products Config instance + */ + public static ProductsConfig getInstance() { + if (instance == null) { + instance = new ProductsConfig(); + } + + return instance; + } + + /** + * Loads user-specified configuration into productCatalogues attribute. + * + * @throws ConfigurationException + */ + public void loadConfig() throws ConfigurationException { + try { + File jsonFile = new File(configLocation).getAbsoluteFile(); + ArrayList> jsonProducts = JsonPath.read(jsonFile, "$.products"); + + setProductCatalogues(jsonProducts); + } catch (IOException e) { + throw new ConfigurationException("Couldn't load required products config file"); + } catch (JsonPathException e) { + throw new ConfigurationException("Products config is empty or malformed"); + } + } + + /** + * Loads catalogues from JSON. + * + * @param jsonCatalogues + */ + private void setProductCatalogues(final ArrayList> jsonCatalogues) { + for (HashMap productCatalogue : jsonCatalogues) { + String name = productCatalogue.get("name").toString(); + String desc = productCatalogue.get("description").toString(); + String identifiers = productCatalogue.get("identifiers").toString(); + + ProductCatalogue pCat = new ProductCatalogue(name, desc); + + addProductsToCatalogue(identifiers, pCat); + productCatalogues.add(pCat); + } + } + + /** + * Loads products from JSON. + * + * @param jsonIdentifiers + * @param pCat + */ + private void addProductsToCatalogue(final String jsonIdentifiers, final ProductCatalogue pCat) { + ArrayList> identifiers = JsonPath.read(jsonIdentifiers, "$"); + + for (HashMap product : identifiers) { + String dataOrigin = product.keySet().iterator().next(); + String identifier = product.get(dataOrigin).toString(); + + Product newProduct = new Product(); + newProduct.setDataOrigin(dataOrigin); + newProduct.setProductId(identifier); + + pCat.addProduct(newProduct); + } + } + + /** + * Resets the contained product catalogues. + */ + public void reset() { + productCatalogues = new ArrayList<>(); + } +} diff --git a/src/main/java/de/rwu/easydrop/util/package-info.java b/src/main/java/de/rwu/easydrop/util/package-info.java index 40789f2..ed8e5eb 100644 --- a/src/main/java/de/rwu/easydrop/util/package-info.java +++ b/src/main/java/de/rwu/easydrop/util/package-info.java @@ -1,6 +1,6 @@ /** * General utility such as formatting helpers. * - * TODO implement + * @since 0.1.0 */ package de.rwu.easydrop.util; diff --git a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java index c84df2a..f1c85b0 100644 --- a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java @@ -1,6 +1,8 @@ package de.rwu.easydrop.api.client; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; import javax.naming.ConfigurationException; @@ -9,7 +11,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.sqlite.SQLiteDataSource; +import de.rwu.easydrop.data.connector.SQLiteConnector; +import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.util.Config; class DataSourceFactoryTest { @@ -45,4 +50,20 @@ class DataSourceFactoryTest { // Assert assertEquals("ebay-api-key", dataSource.getApiKey()); } + + @Test + void createProductPersistenceDataSource_NullPersistence() { + PersistenceException exception = assertThrows(PersistenceException.class, () -> { + dataSourceFactory.createProductPersistenceDataSource(); + }); + + assertEquals("Persistence is not set", exception.getMessage()); + } + + @Test + void createProductPersistenceDataSource_WorkingPersistence() { + dataSourceFactory.setPersistence(new SQLiteConnector(new SQLiteDataSource())); + + assertDoesNotThrow(() -> dataSourceFactory.createProductPersistenceDataSource()); + } } diff --git a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java new file mode 100644 index 0000000..2e54299 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java @@ -0,0 +1,195 @@ +package de.rwu.easydrop.data.connector; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doThrow; + +import java.sql.SQLException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sqlite.SQLiteDataSource; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.PersistenceException; + +@TestInstance(Lifecycle.PER_CLASS) +class SQLiteConnectorTest { + private static final String TEST_PRODUCT_ID = "12345"; + private SQLiteConnector sqliteConnector; + + @Mock + private SQLiteDataSource mockDataSource; + + @BeforeAll + public void setup() { + sqliteConnector = new SQLiteConnector(new SQLiteDataSource()); + } + + @BeforeEach + public void clearDatabase() { + MockitoAnnotations.openMocks(this); + } + + @Test + void saveProduct_ValidProduct_SuccessfullySaved() { + // Arrange + sqliteConnector.clearData(); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + ProductDTO.setDataOrigin("Amazon"); + ProductDTO.setProductId(TEST_PRODUCT_ID); + ProductDTO.setCurrentPrice(9.99); + ProductDTO.setMerchant("Sample Merchant"); + ProductDTO.setDeliveryPrice(2.50); + ProductDTO.setAvailable(true); + + // Act + assertDoesNotThrow(() -> sqliteConnector.saveProduct(ProductDTO)); + + // Assert + ProductDTO savedProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID); + assertNotNull(savedProductDTO); + assertEquals("Amazon", savedProductDTO.getDataOrigin()); + assertEquals(TEST_PRODUCT_ID, savedProductDTO.getProductId()); + assertEquals(9.99, savedProductDTO.getCurrentPrice()); + assertEquals("Sample Merchant", savedProductDTO.getMerchant()); + assertEquals(2.50, savedProductDTO.getDeliveryPrice()); + assertTrue(savedProductDTO.isAvailable()); + } + + @Test + void getProductDTOById_ProductExists_ReturnsProductDTO() { + // Arrange + sqliteConnector.clearData(); + insertSampleProduct(); + + // Act + ProductDTO ProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID); + + // Assert + assertNotNull(ProductDTO); + assertEquals("Amazon", ProductDTO.getDataOrigin()); + assertEquals(TEST_PRODUCT_ID, ProductDTO.getProductId()); + assertEquals(9.99, ProductDTO.getCurrentPrice()); + assertEquals("Sample Merchant", ProductDTO.getMerchant()); + assertEquals(2.50, ProductDTO.getDeliveryPrice()); + assertTrue(ProductDTO.isAvailable()); + } + + @Test + void constructor_ThrowsPersistenceException_OnSQLException() { + try { + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + PersistenceException exception = assertThrows(PersistenceException.class, () -> { + new SQLiteConnector(mockDataSource); + }); + + assertEquals("Something went wrong while initializing SQLite DB", exception.getMessage()); + } catch (SQLException e) { + fail("No SQLException should be thrown"); + } + } + + @Test + void getProductDTOById_ProductDoesNotExist_ReturnsNull() { + // Act + ProductDTO ProductDTO = sqliteConnector.getProductDTOById("FAKE_ID"); + + // Assert + assertNull(ProductDTO); + } + + @Test + void saveProduct_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + assertThrows(PersistenceException.class, () -> sqliteConnector.saveProduct(ProductDTO)); + } + + @Test + void getProductDTOById_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + String productId = "12345"; + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + assertThrows(PersistenceException.class, () -> sqliteConnector.getProductDTOById(productId)); + } + + @Test + void clearData_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + assertThrows(PersistenceException.class, () -> sqliteConnector.clearData()); + } + + private void insertSampleProduct() { + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + ProductDTO.setCurrentPrice(9.99); + ProductDTO.setMerchant("Sample Merchant"); + ProductDTO.setDeliveryPrice(2.50); + ProductDTO.setAvailable(true); + sqliteConnector.saveProduct(ProductDTO); + } + + @Test + void getDataOrigin_ReturnsCorrectDataOrigin() { + // Arrange + SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + + // Act + String dataOrigin = connector.getDataOrigin(); + + // Assert + assertEquals("SQLite", dataOrigin); + } + + @Test + void getApiKey_UnsupportedOperationExceptionThrown() { + // Arrange + SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + + // Act and Assert + assertThrows(UnsupportedOperationException.class, connector::getApiKey); + } + + @Test + void buildProductDTO_UnsupportedOperationExceptionThrown() { + // Arrange + SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + ProductDTO product = new ProductDTO("ASIN123", "Amazon"); + String json = "{\"productId\":\"ASIN123\",\"dataOrigin\":\"Amazon\"}"; + + // Act and Assert + assertThrows(UnsupportedOperationException.class, () -> connector.buildProductDTO(product, json)); + } + + @Test + void createApiUrl_UnsupportedOperationExceptionThrown() { + // Arrange + SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + String productIdentifier = "ASIN123"; + + // Act and Assert + assertThrows(UnsupportedOperationException.class, () -> connector.createApiUrl(productIdentifier)); + } +} diff --git a/src/test/java/de/rwu/easydrop/exception/PersistenceExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/PersistenceExceptionTest.java new file mode 100644 index 0000000..b5d2ae2 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/PersistenceExceptionTest.java @@ -0,0 +1,28 @@ +package de.rwu.easydrop.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class PersistenceExceptionTest { + + @Test + void testPersistenceExceptionWithMessage() { + String errorMessage = "Error occurred during data persistence."; + PersistenceException exception = new PersistenceException(errorMessage); + + assertEquals(errorMessage, exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testPersistenceExceptionWithMessageAndCause() { + String errorMessage = "Error occurred during data persistence."; + Throwable cause = new IllegalArgumentException("Invalid argument."); + PersistenceException exception = new PersistenceException(errorMessage, cause); + + assertEquals(errorMessage, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } +} diff --git a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java new file mode 100644 index 0000000..f070e2d --- /dev/null +++ b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java @@ -0,0 +1,93 @@ +package de.rwu.easydrop.model; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ProductCatalogueTest { + private ProductCatalogue productCatalogue; + + @BeforeEach + public void setup() { + productCatalogue = new ProductCatalogue("GPU", "Graphics Processing Units"); + } + + @Test + void testAddProduct() { + Product product = new Product(); + product.setProductId("12345"); + product.setMerchant("AmazonSeller"); + product.setDataOrigin("Amazon"); + productCatalogue.addProduct(product); + + List products = productCatalogue.getProducts(); + Assertions.assertEquals(1, products.size()); + Assertions.assertEquals(product, products.get(0)); + } + + @Test + void testRemoveProduct() { + Product product1 = new Product(); + product1.setProductId("12345"); + product1.setMerchant("AmazonSeller"); + product1.setDataOrigin("Amazon"); + productCatalogue.addProduct(product1); + + Product product2 = new Product(); + product2.setProductId("54321"); + product2.setMerchant("eBaySeller"); + product2.setDataOrigin("eBay"); + productCatalogue.addProduct(product2); + + productCatalogue.removeProduct(product1); + + List products = productCatalogue.getProducts(); + Assertions.assertEquals(1, products.size()); + Assertions.assertEquals(product2, products.get(0)); + } + + @Test + void testClearProducts() { + Product product1 = new Product(); + product1.setProductId("12345"); + product1.setMerchant("AmazonSeller"); + product1.setDataOrigin("Amazon"); + productCatalogue.addProduct(product1); + + Product product2 = new Product(); + product2.setProductId("54321"); + product2.setMerchant("eBay"); + product2.setDataOrigin("eBay"); + productCatalogue.addProduct(product2); + + productCatalogue.clearProducts(); + + List products = productCatalogue.getProducts(); + Assertions.assertTrue(products.isEmpty()); + } + + @Test + void testToString() { + Product product1 = new Product(); + product1.setProductId("12345"); + product1.setMerchant("AmazonSeller"); + product1.setDataOrigin("Amazon"); + productCatalogue.addProduct(product1); + + Product product2 = new Product(); + product2.setProductId("54321"); + product2.setMerchant("eBaySeller"); + product2.setDataOrigin("eBay"); + productCatalogue.addProduct(product2); + + String expectedString = "Product Catalogue: GPU\n" + + "Description: Graphics Processing Units\n" + + "Products:\n" + + "Product: [12345 from AmazonSeller (Amazon) at 0,00 € (available: no)]\n" + + "Product: [54321 from eBaySeller (eBay) at 0,00 € (available: no)]\n"; + + Assertions.assertEquals(expectedString, productCatalogue.toString()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java index 6dbf741..6239dbf 100644 --- a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java +++ b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java @@ -11,6 +11,7 @@ import java.lang.reflect.Modifier; import org.junit.jupiter.api.Test; import de.rwu.easydrop.api.dto.ProductDTO; + import de.rwu.easydrop.model.Product; class ProductMapperTest { @@ -53,4 +54,27 @@ class ProductMapperTest { dto.setMerchant("Example Merchant"); return dto; } + + @Test + void mapProductToDTO() { + // Arrange + Product product = new Product(); + product.setProductId("12345"); + product.setDataOrigin("Amazon"); + product.setAvailable(true); + product.setCurrentPrice(9.99); + product.setDeliveryPrice(2.50); + product.setMerchant("Seller1"); + + // Act + ProductDTO dto = ProductMapper.mapProductToDTO(product); + + // Assert + assertEquals("12345", dto.getProductId()); + assertEquals("Amazon", dto.getDataOrigin()); + assertTrue(dto.isAvailable()); + assertEquals(9.99, dto.getCurrentPrice()); + assertEquals(2.50, dto.getDeliveryPrice()); + assertEquals("Seller1", dto.getMerchant()); + } } diff --git a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java new file mode 100644 index 0000000..acaca80 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java @@ -0,0 +1,103 @@ +package de.rwu.easydrop.service.retriever; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.exception.InvalidProductException; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.util.ProductsConfig; + +class CatalogueRetrieverTest { + private ProductsConfig productsConfig; + private ProductRetriever productRetriever; + private CatalogueRetriever catalogueRetriever; + + @BeforeEach + public void setup() { + productsConfig = mock(ProductsConfig.class); + productRetriever = mock(ProductRetriever.class); + catalogueRetriever = new CatalogueRetriever(productsConfig, productRetriever); + } + + @Test + void loadCatalogues_ValidConfig_CataloguesLoaded() throws ConfigurationException { + // Arrange + List productCatalogues = new ArrayList<>(); + + // Create a sample product catalogue with two products + ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue"); + Product product1 = new Product(); + product1.setDataOrigin("Amazon"); + product1.setProductId("ASIN1"); + productCatalogue.addProduct(product1); + + Product product2 = new Product(); + product2.setDataOrigin("eBay"); + product2.setProductId("ProductID2"); + productCatalogue.addProduct(product2); + productCatalogues.add(productCatalogue); + + // Mock the methods + when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues); + when(productRetriever.getProductFromAmazon("ASIN1")).thenReturn(product1); + when(productRetriever.getProductFromEbay("ProductID2")).thenReturn(product2); + + // Act + catalogueRetriever.loadCatalogues(); + + // Assert + List loadedCatalogues = catalogueRetriever.getProductCatalogues(); + assertEquals(1, loadedCatalogues.size()); + + ProductCatalogue loadedCatalogue = loadedCatalogues.get(0); + assertEquals("Catalogue 1", loadedCatalogue.getProductName()); + assertEquals("Sample catalogue", loadedCatalogue.getDescription()); + List loadedProducts = loadedCatalogue.getProducts(); + assertEquals(2, loadedProducts.size()); + assertEquals(product1, loadedProducts.get(0)); + assertEquals(product2, loadedProducts.get(1)); + + // Verify the method invocations + verify(productsConfig).loadConfig(); + verify(productRetriever).getProductFromAmazon("ASIN1"); + verify(productRetriever).getProductFromEbay("ProductID2"); + } + + @Test + void loadCatalogues_ValidConfig_CataloguesLoaded_InvalidProduct() throws ConfigurationException { + // Arrange + List productCatalogues = new ArrayList<>(); + + // Create a sample product catalogue + ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue"); + + Product product = new Product(); + product.setDataOrigin(""); + product.setProductId("ProductID1"); + productCatalogue.addProduct(product); + productCatalogues.add(productCatalogue); + + // Mock the methods + when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues); + when(productRetriever.getProductFromAmazon("ProductID1")).thenReturn(product); + + // Act and Assert + InvalidProductException exception = assertThrows(InvalidProductException.class, () -> { + catalogueRetriever.loadCatalogues(); + }); + + assertEquals("Product data origin is invalid", exception.getMessage()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java index 36dff9b..32afecc 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java @@ -17,6 +17,7 @@ import de.rwu.easydrop.api.client.AmazonProductDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.util.Config; @@ -33,6 +34,8 @@ class ProductRetrieverTest { private ProductDTO productDTO; @Mock private Product product; + @Mock + private AbstractProductPersistence persistence; private ProductRetriever productRetriever; @@ -84,4 +87,25 @@ class ProductRetrieverTest { assertEquals(9.99, result.getCurrentPrice()); verify(ebayDataSource, times(1)).getProductDTOById(productQuery); } + + @Test + void getProductFromPersistence_ValidProductId_ReturnsProduct() { + // Arrange + String productId = "123"; + when(dataSourceFactory.createProductPersistenceDataSource()).thenReturn(persistence); + when(persistence.getProductDTOById(productId)).thenReturn(productDTO); + when(productDTO.getProductId()).thenReturn(productId); + when(productDTO.getCurrentPrice()).thenReturn(9.99); + when(productDTO.getDataOrigin()).thenReturn("Amazon"); + + // Act + Product result = productRetriever.getProductFromPersistence(productId); + + // Assert + assertEquals(productId, result.getProductId()); + assertEquals(9.99, result.getCurrentPrice()); + + // Verify the interactions + verify(persistence, times(1)).getProductDTOById(productId); + } } diff --git a/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java new file mode 100644 index 0000000..71fdff5 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java @@ -0,0 +1,69 @@ +package de.rwu.easydrop.service.writer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; + +class CatalogueWriterTest { + + @Mock + private AbstractProductPersistence persistenceMock; + + private CatalogueWriter catalogueWriter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + catalogueWriter = new CatalogueWriter(persistenceMock); + } + + @Test + void writeCatalogues_ValidCatalogues_ProductsWrittenToPersistence() { + // Arrange + List catalogues = createSampleCatalogues(); + + // Act + catalogueWriter.writeCatalogues(catalogues); + + // Assert + verify(persistenceMock, times(4)).saveProduct(any(ProductDTO.class)); + } + + private List createSampleCatalogues() { + List catalogues = new ArrayList<>(); + + ProductCatalogue catalogue1 = new ProductCatalogue("Catalogue 1", "Sample catalogue 1"); + catalogue1.addProduct(createSampleProduct("Amazon", "ID 1")); + catalogue1.addProduct(createSampleProduct("eBay", "ID 2")); + + ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue 2", "Sample catalogue 2"); + catalogue2.addProduct(createSampleProduct("Amazon", "ID 3")); + catalogue2.addProduct(createSampleProduct("eBay", "ID 4")); + + catalogues.add(catalogue1); + catalogues.add(catalogue2); + + return catalogues; + } + + private Product createSampleProduct(String dataOrigin, String productId) { + Product product = new Product(); + product.setDataOrigin(dataOrigin); + product.setProductId(productId); + product.setCurrentPrice(9999.99); + return product; + } +} diff --git a/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java new file mode 100644 index 0000000..aab0cdd --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java @@ -0,0 +1,55 @@ +package de.rwu.easydrop.service.writer; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Product; + +class ProductWriterTest { + + @Mock + private AbstractProductPersistence persistence; + + private ProductWriter productWriter; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + productWriter = new ProductWriter(); + productWriter.setPersistence(persistence); + } + + @Test + void writeProductToPersistence_ValidProduct_CallsSaveProduct() { + // Arrange + Product product = new Product(); + product.setProductId("12345"); + product.setDataOrigin("Amazon"); + product.setCurrentPrice(9.99); + + // Act + productWriter.writeProductToPersistence(product); + + // Assert + Mockito.verify(persistence).saveProduct(any(ProductDTO.class)); + } + + @Test + void writeProductToPersistence_InvalidProduct_ThrowsException() { + // Arrange + Product product = new Product(); + product.setProductId(""); + product.setDataOrigin("Amazon"); + + // Act and Assert + assertThrows(Exception.class, () -> productWriter.writeProductToPersistence(product)); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ConfigIntegrationTest.java similarity index 93% rename from src/test/java/de/rwu/easydrop/util/ConfigImplTest.java rename to src/test/java/de/rwu/easydrop/util/ConfigIntegrationTest.java index 59cece7..2df710d 100644 --- a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java +++ b/src/test/java/de/rwu/easydrop/util/ConfigIntegrationTest.java @@ -12,9 +12,9 @@ import javax.naming.ConfigurationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class ConfigImplTest { +class ConfigIntegrationTest { private Config config; - private final static String TESTDATA_PATH = "src/test/resources/testdata.properties"; + private final static String TESTDATA_PATH = "src/test/resources/test.config.properties"; private final static String TESTDATA_KEY = "API_KEY"; private final static String TESTDATA_VAL = "keyIsHere"; @@ -77,7 +77,7 @@ class ConfigImplTest { @Test void testLoadConfigSuccessfully() { try { - config.setConfigLocation("src/test/resources/testdata.properties"); + config.setConfigLocation("src/test/resources/test.config.properties"); config.loadConfig(); assertEquals(TESTDATA_VAL, config.getProperty(TESTDATA_KEY)); } catch (ConfigurationException e) { @@ -98,7 +98,7 @@ class ConfigImplTest { @Test void testReset() throws ConfigurationException { - config.setConfigLocation("src/test/resources/testdata.properties"); + config.setConfigLocation("src/test/resources/test.config.properties"); config.loadConfig(); assertNotNull(config.getProperty(TESTDATA_KEY)); diff --git a/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java b/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java new file mode 100644 index 0000000..a415933 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java @@ -0,0 +1,89 @@ +package de.rwu.easydrop.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.model.ProductCatalogue; + +class ProductsConfigIntegrationTest { + private ProductsConfig config; + private final static String TESTDATA_PATH = "src/test/resources/test.products-config.json"; + private final static String TESTDATA_EMPTY_PATH = "src/test/resources/test.empty.products-config.json"; + private final static String TESTDATA_MALFORMED_PATH = "src/test/resources/test.malformed.products-config.json"; + + @BeforeEach + void setUp() { + config = ProductsConfig.getInstance(); + } + + @Test + void testLoadConfigSuccessfully() { + try { + config.setConfigLocation(TESTDATA_PATH); + config.loadConfig(); + String value = config.getProductCatalogues().get(0).getProductName(); + + assertEquals("Demo Product", value); + } catch (ConfigurationException e) { + fail("ConfigurationException should not be thrown"); + } + } + + @Test + void testLoadConfigMissingFile() { + config.setConfigLocation("path/that/doesnt/exist/products-config.json"); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + config.loadConfig(); + }); + + assertEquals("Couldn't load required products config file", exception.getMessage()); + } + + @Test + void testLoadConfigEmptyFile() { + config.setConfigLocation(TESTDATA_EMPTY_PATH); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + config.loadConfig(); + }); + + assertEquals("Products config is empty or malformed", exception.getMessage()); + } + + @Test + void testLoadConfigMalformedFile() { + config.setConfigLocation(TESTDATA_MALFORMED_PATH); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + config.loadConfig(); + }); + + assertEquals("Products config is empty or malformed", exception.getMessage()); + } + + @Test + void testReset() { + try { + config.setConfigLocation(TESTDATA_PATH); + config.loadConfig(); + } catch (ConfigurationException e) { + fail("ConfigurationException should not be thrown"); + } + + assertNotNull(config.getProductCatalogues()); + config.reset(); + List pCats = config.getProductCatalogues(); + + assertEquals(0, pCats.size(), "Catalogues list should be empty"); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/ProductsConfigTest.java b/src/test/java/de/rwu/easydrop/util/ProductsConfigTest.java new file mode 100644 index 0000000..b791d2a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/util/ProductsConfigTest.java @@ -0,0 +1,44 @@ +package de.rwu.easydrop.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.spy; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +class ProductsConfigTest { + private ProductsConfig productsConfig; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + productsConfig = spy(ProductsConfig.getInstance()); + } + + @Test + void testGetInstance() { + ProductsConfig newConfig = ProductsConfig.getInstance(); + assertNotNull(newConfig); + } + + @Test + void testGetInstanceEquality() { + // Create "two" instances to check validity of Singleton pattern + ProductsConfig instance1 = ProductsConfig.getInstance(); + ProductsConfig instance2 = ProductsConfig.getInstance(); + + assertSame(instance1, instance2, "Instances should be equal"); + } + + @Test + void testSetConfigLocation() { + String newPath = "new/location/config.properties"; + productsConfig.setConfigLocation(newPath); + assertEquals(newPath, productsConfig.getConfigLocation()); + } +} diff --git a/src/test/resources/testdata.properties b/src/test/resources/test.config.properties similarity index 100% rename from src/test/resources/testdata.properties rename to src/test/resources/test.config.properties diff --git a/src/test/resources/test.empty.products-config.json b/src/test/resources/test.empty.products-config.json new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/empty.properties b/src/test/resources/test.empty.properties similarity index 100% rename from src/test/resources/empty.properties rename to src/test/resources/test.empty.properties diff --git a/src/test/resources/test.malformed.products-config.json b/src/test/resources/test.malformed.products-config.json new file mode 100644 index 0000000..00d0092 --- /dev/null +++ b/src/test/resources/test.malformed.products-config.json @@ -0,0 +1,15 @@ +{ + "products": [ + { + "name": "Demo Product", + description: "Integration Testing Product", + "identifiers": [ + { + "Amazon": "DEMO-AMAZON-001" + }, + { + "eBay": "DEMO-EBAY-001" + } + ] + ] +} diff --git a/src/test/resources/test.products-config.json b/src/test/resources/test.products-config.json new file mode 100644 index 0000000..44262c8 --- /dev/null +++ b/src/test/resources/test.products-config.json @@ -0,0 +1,16 @@ +{ + "products": [ + { + "name": "Demo Product", + "description": "Integration Testing Product", + "identifiers": [ + { + "Amazon": "DEMO-AMAZON-001" + }, + { + "eBay": "DEMO-EBAY-001" + } + ] + } + ] +}