diff --git a/.vscode/settings.json b/.vscode/settings.json index b72b3e1..d861443 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "connectionId": "LocalSonarQube", "projectKey": "EasyDrop" }, - "java.debug.settings.onBuildFailureProceed": true + "java.debug.settings.onBuildFailureProceed": true, + "java.checkstyle.configuration": "${workspaceFolder}\\config\\custom-checkstyle.xml" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 5498b1c..1ff02be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,18 @@ # Changelog -## 0.3.0-SNAPSHOT +## 1.0.0 ### Added -- Added script for local UML generation (#56) +- Script for local UML generation (#56) +- Transaction fulfillment API classes (#71) +- Offer handling classes (#12, #64, #70) +- Transaction handling classes (#75) +- Dockerfile and `docker-compose.yml` (#73) -### Removed +### Changed -- Document stage from CI run (#56) +- Webshops are now handled via enum instead of being hard-coded ## 0.2.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dc39d04 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Use the official maven/Java 17 image to create a build artifact. +# https://hub.docker.com/_/maven +FROM maven:3.9.1-eclipse-temurin-17 AS build + +# Set the working directory in the image to /app +WORKDIR /app + +# Copy the pom.xml file into the current directory (/app) in the image +COPY pom.xml . + +# Download all required dependencies into one layer +RUN mvn -B dependency:go-offline + +# Copy the rest of the application source code +COPY src /app/src + +# Build the application +RUN mvn -B package -DskipTests + +# Use OpenJDK 17 for the runtime stage of the Dockerfile +FROM openjdk:17-jdk-slim + +# Copy the jar file from the build stage +COPY --from=build /app/target/easydrop-1.0.0.jar /easydrop.jar + +# Execute the application when the docker container starts. +ENTRYPOINT ["java", "-jar", "/easydrop.jar"] + diff --git a/Docs/DemoImage.png b/Docs/DemoImage.png index 18fb11f..d672e79 100644 Binary files a/Docs/DemoImage.png and b/Docs/DemoImage.png differ diff --git a/README.md b/README.md index e465e36..00cf601 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # EasyDrop -![Version](https://img.shields.io/badge/version-0.3.0_SNAPSHOT-orange) +![Version](https://img.shields.io/badge/version-1.0.0-orange) ![Pipeline](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/badges/main/pipeline.svg) [![Coverage](https://sonar.fbe-adswen.rwu.de/api/project_badges/measure?project=de.rwu%3Aeasydrop&metric=coverage&token=sqb_2fe80aed361468170aaef32a0ff96d596456cdd1)](https://sonar.fbe-adswen.rwu.de/dashboard?id=de.rwu%3Aeasydrop) ![JAMANN](https://img.shields.io/badge/Auszahlung-Letzte%20Woche-brightgreen) @@ -13,30 +13,33 @@ Get rich quickβ„’ with fully automated dropshipping! ![Demo Image](Docs/DemoImage.png) -## Installation βš™ +## Installation/Usage βš™ -As the application is still in development, development software will be required along its regular execution environment. - -- JDK 17 -- Maven -- _[More/context](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/wikis/Projektdokumentation/Development#software)_ - -## Usage πŸ›  +- Install Docker + Compose +- Clone this repository Set up the required configuration files, use the corresponding demo files for orientation. - `config/config.properties` for API authorization - `products-config.json` to define product catalogues to use for dropshipping -Run the following command to execute the program: +Create the container -```sh -mvn compile exec:java -Dexec.mainClass="de.rwu.easydrop.Main" +```bash +docker-compose build ``` -## Roadmap 🏁 +Then run it! -_Future ideas!_ +```bash +docker-compose up +``` + +To run it on a fixed schedule, install a cron job, e.g. + +```bash +*/30 * * * * cd /home/user/easydrop && docker-compose up -d +``` ## Contributing πŸ‘·β€β™‚οΈπŸ‘·β€β™€οΈ diff --git a/Script/Bump_Version.sh b/Script/Bump_Version.sh new file mode 100644 index 0000000..190506d --- /dev/null +++ b/Script/Bump_Version.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Checking if correct number of arguments are provided +if [ "$#" -ne 2 ]; then + echo "Required CLI args missing: [old_version, new_version]" + exit 1 +fi + +# Assigning command line arguments to variables +old_version="$1" +new_version="$2" + +old_version_alt=${old_version//-/–} +new_version_alt=${new_version//-/–} + +# Replacing old_version with new_version in README.md +sed -i "s/version-$old_version_alt-orange/version-$new_version_alt-orange/g" README.md +sed -i "s/$old_version/$new_version/g" pom.xml +sed -i "s/easydrop-$old_version.jar/easydrop-$new_version.jar/g" Dockerfile +sed -i "s/image: easydrop:$old_version/image: easydrop:$new_version/g" docker-compose.yml + +echo "Version number updated successfully" + diff --git a/Script/SonarQube_Local.sh b/Script/SonarQube_Local.sh index 14d2602..8517553 100644 --- a/Script/SonarQube_Local.sh +++ b/Script/SonarQube_Local.sh @@ -2,6 +2,5 @@ mvn clean verify sonar:sonar -Pcoverage \ -Dsonar.projectKey=EasyDrop \ -Dsonar.projectName='EasyDrop' \ -Dsonar.host.url=http://localhost:9000 \ - -Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931 - -start http://localhost:9000/dashboard?id=EasyDrop \ 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/custom-checkstyle.xml b/config/custom-checkstyle.xml index 71d9e97..21428fc 100644 --- a/config/custom-checkstyle.xml +++ b/config/custom-checkstyle.xml @@ -44,6 +44,11 @@ https://www.oracle.com/java/technologies/javase/codeconventions-contents.html + + + + + diff --git a/config/demo.config.properties b/config/demo.config.properties index d3e4d63..13b994e 100644 --- a/config/demo.config.properties +++ b/config/demo.config.properties @@ -2,4 +2,5 @@ AMAZON_API_URL= AMAZON_API_KEY= EBAY_API_URL= -EBAY_API_KEY= \ No newline at end of file +EBAY_API_KEY= +WEBSHOPS=amazon,ebay \ No newline at end of file diff --git a/config/demo.products-config.json b/config/demo.products-config.json index 2aa9058..0474928 100644 --- a/config/demo.products-config.json +++ b/config/demo.products-config.json @@ -5,10 +5,34 @@ "description": "Very epic GPU", "identifiers": [ { - "Amazon": "B096Y2TYKV" + "AMAZON": "B096Y2TYKV" }, { - "eBay": "Gigabyte GeForce RTX 3060" + "EBAY": "Gigabyte GeForce RTX 3060" + } + ] + }, + { + "name": "Gigabyte Radeon RX 6700 XT", + "description": "Another one", + "identifiers": [ + { + "AMAZON": "B08Y758F6C" + }, + { + "EBAY": "Gigabyte Radeon RX 6700 XT" + } + ] + }, + { + "name": "NVIDIA GeForce RTX 4060 Ti", + "description": "Premium!", + "identifiers": [ + { + "AMAZON": "B0C4F9HCF6" + }, + { + "EBAY": "NVIDIA GeForce RTX 4060 Ti" } ] } diff --git a/config/suppressions.xml b/config/suppressions.xml new file mode 100644 index 0000000..4067d19 --- /dev/null +++ b/config/suppressions.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e424c90 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.8" + +services: + easydrop: + container_name: easydrop + build: + context: . + dockerfile: Dockerfile + image: easydrop:1.0.0 + volumes: + - ./config:/config + - ./persistence.db:/persistence.db diff --git a/pom.xml b/pom.xml index 587ec28..3549a32 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ de.rwu easydrop jar - 0.3.0-SNAPSHOT + 1.0.0 EasyDrop http://maven.apache.org @@ -86,6 +86,27 @@ + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + de.rwu.easydrop.Main + + + + + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index 5441d96..db1fedb 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -1,22 +1,8 @@ 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; +import de.rwu.easydrop.core.Core; /** * Kickoff point for the service. @@ -24,11 +10,6 @@ import de.rwu.easydrop.util.ProductsConfig; * @since 0.1.0 */ public final class Main { - /** - * Logger for main process. - */ - private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); - /** * Prevents unwanted instantiation. */ @@ -42,21 +23,6 @@ public final class Main { * @param args */ 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); - - catRetriever.loadCatalogues(); - List pCats = catRetriever.getProductCatalogues(); - catWriter.writeCatalogues(pCats); - - for (ProductCatalogue pCat : pCats) { - String pCatStr = pCat.toString(); - LOGGER.info(pCatStr); - } + Core.run(); } } diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java index 7751c10..bfdad1a 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java @@ -9,6 +9,7 @@ import java.net.URL; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.DataSourceException; +import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.util.FormattingUtil; /** @@ -23,7 +24,7 @@ public abstract class AbstractDataSource implements DataSource { * * @return Data source name */ - protected abstract String getDataOrigin(); + protected abstract Webshop getDataOrigin(); /** * Returns the data source's API key. @@ -42,13 +43,13 @@ public abstract class AbstractDataSource implements DataSource { protected abstract ProductDTO buildProductDTO(ProductDTO product, String json); /** - * Overridable standard implementation. + * Pulls product data from a remote data source. */ @Override public ProductDTO getProductDTOById(final String productIdentifier) throws IllegalArgumentException { StringBuilder response = new StringBuilder(); - String dataOrigin = getDataOrigin(); + Webshop dataOrigin = getDataOrigin(); String apiKey = getApiKey(); ProductDTO product = new ProductDTO(productIdentifier, dataOrigin); @@ -78,7 +79,13 @@ public abstract class AbstractDataSource implements DataSource { } reader.close(); - buildProductDTO(product, response.toString()); + String data; + if (response.toString().isEmpty()) { + data = "{}"; + } else { + data = response.toString(); + } + buildProductDTO(product, data); } catch (IOException e) { throw new DataSourceException( "Couldn't fulfill " diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java b/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java new file mode 100644 index 0000000..6357230 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataWriter.java @@ -0,0 +1,82 @@ +package de.rwu.easydrop.api.client; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.DataWriterException; +import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.util.FormattingUtil; + +/** + * Writes data to a remote API. + * + * @since 0.3.0 + */ +public abstract class AbstractDataWriter { + + /** + * Returns the data source's API key. + * + * @return Data source API key + */ + protected abstract String getApiKey(); + + /** + * Returns the target API's name. + * + * @return Data target API name + */ + protected abstract Webshop getDataTarget(); + + /** + * Creates an URL object to connect to the API with. + * + * @param productIdentifier Product identifier + * @return URL object + * @throws MalformedURLException + */ + protected abstract URL createApiUrl(String productIdentifier) throws MalformedURLException; + + /** + * Sends a put request to the API. + * + * @param dto Product data transfer object + * @param apiType API Type, like Purchase or Sales + */ + protected void sendPutRequest(final ProductDTO dto, final String apiType) { + String apiKey = getApiKey(); + Webshop dataTarget = getDataTarget(); + + if (!dataTarget.equals(dto.getDataOrigin())) { + throw new DataWriterException( + "Product data source and target " + apiType + " API are incompatible."); + } + + try { + String urlReadyIdentifier = FormattingUtil.urlEncode(dto.getProductId()); + URL apiUrl = createApiUrl(urlReadyIdentifier); + + HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod("PUT"); + connection.setRequestProperty("Credential", apiKey); + + int responseCode = connection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new DataWriterException( + dataTarget + + " " + + apiType + + " API responded with error code " + + responseCode); + } + } catch (IOException e) { + throw new DataWriterException( + "Couldn't fulfill " + + dataTarget + + " API request"); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/AbstractPurchaser.java new file mode 100644 index 0000000..67700a0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractPurchaser.java @@ -0,0 +1,19 @@ +package de.rwu.easydrop.api.client; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Abstract construct to provide access to a purchase API. + * + * @since 0.3.0 + */ +public abstract class AbstractPurchaser extends AbstractDataWriter { + /** + * Sends a buy request to the Amazon API. + * + * @param dto + */ + public void purchaseProduct(final ProductDTO dto) { + sendPutRequest(dto, "Purchase"); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractSeller.java b/src/main/java/de/rwu/easydrop/api/client/AbstractSeller.java new file mode 100644 index 0000000..8d659d7 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractSeller.java @@ -0,0 +1,19 @@ +package de.rwu.easydrop.api.client; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Abstract construct to provide access to a sales API. + * + * @since 0.3.0 + */ +public abstract class AbstractSeller extends AbstractDataWriter { + /** + * Sends a sell request to the Amazon API. + * + * @param dto + */ + public void sellProduct(final ProductDTO dto) { + sendPutRequest(dto, "Sales"); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java index 83b758d..64d40ee 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java @@ -8,6 +8,8 @@ import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.ReadContext; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.util.Timestamp; /** * Interface to an Amazon data source. @@ -18,7 +20,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { /** * Name of this data source. */ - private static final String DATA_ORIGIN = "Amazon"; + private static final Webshop DATA_ORIGIN = Webshop.AMAZON; /** * Base URL to the Amazon data source. */ @@ -60,6 +62,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { product.setDeliveryPrice( ctx.read(root + "shippingOptions[0].shippingCost.value.amount", double.class)); product.setMerchant(ctx.read(root + "merchant.name", String.class)); + product.setLastUpdate(Timestamp.now()); } catch (PathNotFoundException e) { // Pass, allow incomplete ProductDTO to pass for later validation } @@ -82,7 +85,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { } @Override - protected String getDataOrigin() { + protected Webshop getDataOrigin() { return DATA_ORIGIN; } diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java new file mode 100644 index 0000000..af96ede --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonPurchaser.java @@ -0,0 +1,69 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +import de.rwu.easydrop.model.Webshop; + +/** + * Sends a buy request to the Amazon API. + * + * @since 0.3.0 + */ +public final class AmazonPurchaser extends AbstractPurchaser { + /** + * Name of this data source. + */ + private static final Webshop DATA_TARGET = Webshop.AMAZON; + /** + * Base URL to the Amazon Purchase API. + */ + private String baseUrl; + /** + * Access credential. + */ + private String apiKey; + /** + * Product region parameter required for data writes. + */ + private static final String PRODUCT_REGION = "DE"; + /** + * Locale parameter required for data writes. + */ + private static final String LOCALE = "de_DE"; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public AmazonPurchaser(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + /** + * @param productId ASIN + */ + @Override + protected URL createApiUrl(final String productId) throws MalformedURLException { + return new URL(baseUrl + + "/products/2020-08-26/products/" + + productId + + "/buy?productRegion=" + + PRODUCT_REGION + + "&locale=" + + LOCALE); + } + + @Override + protected String getApiKey() { + return this.apiKey; + } + + @Override + protected Webshop getDataTarget() { + return DATA_TARGET; + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java b/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java new file mode 100644 index 0000000..42ab6c6 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonSeller.java @@ -0,0 +1,69 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +import de.rwu.easydrop.model.Webshop; + +/** + * Sends a sell request to the Amazon API. + * + * @since 0.3.0 + */ +public final class AmazonSeller extends AbstractSeller { + /** + * Name of this data source. + */ + private static final Webshop DATA_TARGET = Webshop.AMAZON; + /** + * Base URL to the Amazon Purchase API. + */ + private String baseUrl; + /** + * Access credential. + */ + private String apiKey; + /** + * Product region parameter required for data writes. + */ + private static final String PRODUCT_REGION = "DE"; + /** + * Locale parameter required for data writes. + */ + private static final String LOCALE = "de_DE"; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public AmazonSeller(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + /** + * @param productId ASIN + */ + @Override + protected URL createApiUrl(final String productId) throws MalformedURLException { + return new URL(baseUrl + + "/products/2020-08-26/products/" + + productId + + "/sell?productRegion=" + + PRODUCT_REGION + + "&locale=" + + LOCALE); + } + + @Override + protected String getApiKey() { + return apiKey; + } + + @Override + protected Webshop getDataTarget() { + return DATA_TARGET; + } +} 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 490466c..ba79ab5 100644 --- a/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java +++ b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java @@ -2,8 +2,6 @@ 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; /** @@ -17,17 +15,6 @@ 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 @@ -65,17 +52,4 @@ 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/api/client/EbayItemDataSource.java b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java index 171b8b9..f7a205a 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java @@ -8,6 +8,8 @@ import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.ReadContext; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.util.Timestamp; /** * Interface to an eBay data source. @@ -18,7 +20,7 @@ public final class EbayItemDataSource extends AbstractDataSource { /** * Name of this data source. */ - private static final String DATA_ORIGIN = "eBay"; + private static final Webshop DATA_ORIGIN = Webshop.EBAY; /** * Base URL to the eBay data source. */ @@ -69,6 +71,7 @@ public final class EbayItemDataSource extends AbstractDataSource { product.setDeliveryPrice( ctx.read(root + "shippingOptions[0].shippingCost.value", double.class)); product.setMerchant(ctx.read(root + "seller.username", String.class)); + product.setLastUpdate(Timestamp.now()); } catch (PathNotFoundException e) { // Pass, allow incomplete ProductDTO to pass for later validation } @@ -77,7 +80,7 @@ public final class EbayItemDataSource extends AbstractDataSource { } @Override - protected String getDataOrigin() { + protected Webshop getDataOrigin() { return DATA_ORIGIN; } diff --git a/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java b/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java new file mode 100644 index 0000000..67fc08e --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/EbayPurchaser.java @@ -0,0 +1,54 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +import de.rwu.easydrop.model.Webshop; + +/** + * Sends a buy request to the eBay API. + * + * @since 0.3.0 + */ +public final class EbayPurchaser extends AbstractPurchaser { + /** + * Name of this data source. + */ + private static final Webshop DATA_TARGET = Webshop.EBAY; + /** + * Base URL to the eBay Purchase API. + */ + private String baseUrl; + /** + * Access credential. + */ + private String apiKey; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public EbayPurchaser(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + @Override + protected URL createApiUrl(final String searchQuery) throws MalformedURLException { + return new URL(baseUrl + + "/buy/" + + searchQuery); + } + + @Override + protected String getApiKey() { + return apiKey; + } + + @Override + protected Webshop getDataTarget() { + return DATA_TARGET; + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java b/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java new file mode 100644 index 0000000..c09a852 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/EbaySeller.java @@ -0,0 +1,54 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +import de.rwu.easydrop.model.Webshop; + +/** + * Sends a sell request to the eBay API. + * + * @since 0.3.0 + */ +public final class EbaySeller extends AbstractSeller { + /** + * Name of this data source. + */ + private static final Webshop DATA_TARGET = Webshop.EBAY; + /** + * Base URL to the eBay Purchase API. + */ + private String baseUrl; + /** + * Access credential. + */ + private String apiKey; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public EbaySeller(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + @Override + protected URL createApiUrl(final String searchQuery) throws MalformedURLException { + return new URL(baseUrl + + "/sell/" + + searchQuery); + } + + @Override + protected String getApiKey() { + return this.apiKey; + } + + @Override + protected Webshop getDataTarget() { + return DATA_TARGET; + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/PurchaserFactory.java b/src/main/java/de/rwu/easydrop/api/client/PurchaserFactory.java new file mode 100644 index 0000000..24242e4 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/PurchaserFactory.java @@ -0,0 +1,55 @@ +package de.rwu.easydrop.api.client; + +import javax.naming.ConfigurationException; + +import de.rwu.easydrop.util.Config; + +/** + * Factory for Buyer API accessors. + * + * @since 0.3.0 + */ +public class PurchaserFactory { + + /** + * The data source config. + */ + private Config config; + + /** + * @param newConfig the config to set + */ + public void setConfig(final Config newConfig) throws ConfigurationException { + this.config = newConfig; + this.config.loadConfig(); + } + + /** + * @param newConfig + */ + public PurchaserFactory(final Config newConfig) throws ConfigurationException { + this.setConfig(newConfig); + } + + /** + * Creates an Amazon purchaser. + * + * @return AmazonPurchaser + */ + public AmazonPurchaser createAmazonPurchaser() { + String apiUrl = config.getProperty("AMAZON_API_URL"); + String apiKey = config.getProperty("AMAZON_API_KEY"); + return new AmazonPurchaser(apiUrl, apiKey); + } + + /** + * Creates an eBay purchaser. + * + * @return EbayPurchaser + */ + public EbayPurchaser createEbayPurchaser() { + String apiUrl = config.getProperty("EBAY_API_URL"); + String apiKey = config.getProperty("EBAY_API_KEY"); + return new EbayPurchaser(apiUrl, apiKey); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/SellerFactory.java b/src/main/java/de/rwu/easydrop/api/client/SellerFactory.java new file mode 100644 index 0000000..df64a3f --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/SellerFactory.java @@ -0,0 +1,55 @@ +package de.rwu.easydrop.api.client; + +import javax.naming.ConfigurationException; + +import de.rwu.easydrop.util.Config; + +/** + * Factory for Sales API accessors. + * + * @since 0.3.0 + */ +public class SellerFactory { + + /** + * The data source config. + */ + private Config config; + + /** + * @param newConfig the config to set + */ + public void setConfig(final Config newConfig) throws ConfigurationException { + this.config = newConfig; + this.config.loadConfig(); + } + + /** + * @param newConfig + */ + public SellerFactory(final Config newConfig) throws ConfigurationException { + this.setConfig(newConfig); + } + + /** + * Creates an Amazon Seller. + * + * @return AmazonSeller + */ + public AmazonSeller createAmazonSeller() { + String apiUrl = config.getProperty("AMAZON_API_URL"); + String apiKey = config.getProperty("AMAZON_API_KEY"); + return new AmazonSeller(apiUrl, apiKey); + } + + /** + * Creates an eBay Seller. + * + * @return EbaySeller + */ + public EbaySeller createEbaySeller() { + String apiUrl = config.getProperty("EBAY_API_URL"); + String apiKey = config.getProperty("EBAY_API_KEY"); + return new EbaySeller(apiUrl, apiKey); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java new file mode 100644 index 0000000..8dc04d0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/dto/OfferDTO.java @@ -0,0 +1,33 @@ +package de.rwu.easydrop.api.dto; + +import lombok.Data; + +/* + * Offer data transfer object. + * + * @since 0.3.0 + */ +@Data +public class OfferDTO { + /** + * ID of the offer, built from identifiers of the source platforms. + */ + private String offerId; + + /** + * The product that our software buys. + */ + private ProductDTO sourceProduct; + + /** + * The product that our software sells. + */ + private ProductDTO targetProduct; + + /** + * Date of last update of the offer. + */ + private String lastUpdate; + + +} diff --git a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java index f6beb85..00956b1 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java @@ -1,5 +1,6 @@ package de.rwu.easydrop.api.dto; +import de.rwu.easydrop.model.Webshop; import lombok.Data; /** @@ -10,9 +11,9 @@ import lombok.Data; @Data public class ProductDTO { /** - * Data source platform, like "Amazon". + * Data source platform, like Amazon. */ - private String dataOrigin; + private Webshop dataOrigin; /** * Platform internal product identifier. @@ -39,13 +40,18 @@ public class ProductDTO { */ private boolean available; + /** + * Last update from API. + */ + private String lastUpdate; + /** * Creates ProductDTO instance. * * @param newProductId Internal Product indetifier * @param newDataOrigin Data Origin */ - public ProductDTO(final String newProductId, final String newDataOrigin) { + public ProductDTO(final String newProductId, final Webshop newDataOrigin) { this.productId = newProductId; this.dataOrigin = newDataOrigin; } diff --git a/src/main/java/de/rwu/easydrop/api/dto/TransactionDTO.java b/src/main/java/de/rwu/easydrop/api/dto/TransactionDTO.java new file mode 100644 index 0000000..630b2ce --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/dto/TransactionDTO.java @@ -0,0 +1,28 @@ +package de.rwu.easydrop.api.dto; + +import lombok.Data; + +/** + * Transaction data transfer object. + * + * @since 0.3.0 + */ +@Data +public class TransactionDTO { + /** + * Offer ID. + */ + private String offerId; + /** + * Sales volume. + */ + private double volume; + /** + * Earnings (volume - cost). + */ + private double earnings; + /** + * Transaction timestamp. + */ + private String transactionTime; +} diff --git a/src/main/java/de/rwu/easydrop/core/Core.java b/src/main/java/de/rwu/easydrop/core/Core.java new file mode 100644 index 0000000..45f01ef --- /dev/null +++ b/src/main/java/de/rwu/easydrop/core/Core.java @@ -0,0 +1,92 @@ +package de.rwu.easydrop.core; + +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.api.client.PurchaserFactory; +import de.rwu.easydrop.api.client.SellerFactory; +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; +import de.rwu.easydrop.data.connector.SQLiteConnector; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.mapping.OfferMapper; +import de.rwu.easydrop.service.processing.OfferIdentifier; +import de.rwu.easydrop.service.processing.OfferProvisioner; +import de.rwu.easydrop.service.processing.TransactionHandler; +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; + +/** + * The application core. + * + * @since 0.3.0 + */ +public final class Core { + /** + * Logging instance. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(Core.class); + + /** + * Hidden Constructor. + */ + private Core() { + // Hidden constructor + } + + /** + * Runs the core. + * + * @throws ConfigurationException + */ + public static void run() throws ConfigurationException { + LOGGER.info("Loading config..."); + Config config = Config.getInstance(); + ProductsConfig pConfig = ProductsConfig.getInstance(); + + LOGGER.info("Preparing..."); + DataSourceFactory dataSourceFactory = new DataSourceFactory(config); + SellerFactory sellerFactory = new SellerFactory(config); + PurchaserFactory purchaserFactory = new PurchaserFactory(config); + + ProductPersistenceInterface productDB = new SQLiteConnector(new SQLiteDataSource()); + OfferPersistenceInterface offerDB = new SQLiteConnector(new SQLiteDataSource()); + TransactionPersistenceInterface txDB = new SQLiteConnector(new SQLiteDataSource()); + + ProductRetriever retriever = new ProductRetriever(dataSourceFactory, productDB); + CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); + CatalogueWriter catWriter = new CatalogueWriter(productDB); + + LOGGER.info("Loading catalogues"); + catRetriever.loadCatalogues(); + List pCats = catRetriever.getProductCatalogues(); + catWriter.writeCatalogues(pCats); + + LOGGER.info("Creating offers"); + OfferIdentifier ident = new OfferIdentifier(); + OfferProvisioner provis = new OfferProvisioner(offerDB, sellerFactory, purchaserFactory); + List identifiedOffers = ident.runIdentifier(pCats); + provis.runProvisioner(identifiedOffers); + + LOGGER.info("Creating transactions"); + TransactionHandler txHandler = new TransactionHandler(txDB); + for (OfferDTO o : offerDB.getOffers()) { + txHandler.turnOfferToTransaction(OfferMapper.mapOfferFromDTO(o)); + } + + txDB.outputTransactionsToLog(); + txDB.outputSummaryToLog(); + } +} diff --git a/src/main/java/de/rwu/easydrop/core/package-info.java b/src/main/java/de/rwu/easydrop/core/package-info.java new file mode 100644 index 0000000..c0963c1 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/core/package-info.java @@ -0,0 +1,6 @@ +/** + * EasyDrop's core. + * + * @since 0.3.0 + */ +package de.rwu.easydrop.core; diff --git a/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java b/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java deleted file mode 100644 index 6a302dc..0000000 --- a/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java +++ /dev/null @@ -1,34 +0,0 @@ -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/OfferPersistenceInterface.java b/src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java new file mode 100644 index 0000000..d0f04d8 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/OfferPersistenceInterface.java @@ -0,0 +1,46 @@ +package de.rwu.easydrop.data.connector; + +import java.util.List; + +import de.rwu.easydrop.api.dto.OfferDTO; + +/** + * Interface to offer persistence. + * + * @since 0.3.0 + */ +public interface OfferPersistenceInterface { + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + void writeOffer(OfferDTO dto); + + /** + * Gets a OfferDTO from persistence. + * + * @param offerId + * @return Offer data transfer object + */ + OfferDTO getOfferDTOById(String offerId); + + /** + * Gets all offerDTOs from persistence. + * + * @return offerDTOs + */ + List getOffers(); + + /** + * Deletes all offer data from persistence. + */ + void clearOfferData(); + + /** + * Deletes an offer from persistence. + * + * @param offerId + */ + void deleteOfferById(String offerId); +} diff --git a/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java b/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java new file mode 100644 index 0000000..45ec2bd --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/ProductPersistenceInterface.java @@ -0,0 +1,30 @@ +package de.rwu.easydrop.data.connector; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Allows connecting to a persistent product data store. + * + * @since 0.2.0 + */ +public interface ProductPersistenceInterface { + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + void writeProduct(ProductDTO dto); + + /** + * Gets a ProductDTO from persistence. + * + * @param productId Product identifier + * @return Product data transfer object + */ + ProductDTO getProductDTOById(String productId); + + /** + * Deletes all product data from persistence. + */ + void clearProductData(); +} diff --git a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java index 6947c5d..3b110b5 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -1,27 +1,41 @@ 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 java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sqlite.SQLiteDataSource; +import de.rwu.easydrop.api.dto.OfferDTO; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.api.dto.TransactionDTO; import de.rwu.easydrop.exception.PersistenceException; +import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.util.FormattingUtil; /** * Allows connecting to a SQLite Database. * * @since 0.2.0 */ -public final class SQLiteConnector extends AbstractProductPersistence { +public final class SQLiteConnector implements + ProductPersistenceInterface, OfferPersistenceInterface, + TransactionPersistenceInterface { /** - * Data origin. + * Base for calculating percentages. */ - private static final String DATA_ORIGIN = "SQLite"; + private static final int PERCENT = 100; + + /** + * Logging instance. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(SQLiteConnector.class); /** * SQLite Database. @@ -39,6 +53,15 @@ public final class SQLiteConnector extends AbstractProductPersistence { * Path to SQLite db file. */ private static final String PERSISTENCE_PATH = "jdbc:sqlite:persistence.db"; + /** + * Name of 'lastUpdate' column. + */ + private static final String LAST_UPDATE_COL_NAME = "lastUpdate"; + + /** + * Name of 'offerId' column. + */ + private static final String OFFERID_COL_NAME = "offerId"; /** * Creates instance. @@ -57,8 +80,8 @@ public final class SQLiteConnector extends AbstractProductPersistence { Connection connection = db.getConnection(); // Execute SQL statements to create tables - Statement statement = connection.createStatement(); - statement.execute( + Statement createProducts = connection.createStatement(); + createProducts.execute( "CREATE TABLE IF NOT EXISTS products (" + "dataOrigin TEXT, " + "productId TEXT, " @@ -66,12 +89,39 @@ public final class SQLiteConnector extends AbstractProductPersistence { + "merchant TEXT, " + "deliveryPrice REAL, " + "available INT, " - + "lastupdate TEXT, " + + "lastUpdate TEXT, " + "UNIQUE(productId, dataOrigin) ON CONFLICT REPLACE" + ")"); - // Close the statement and connection - statement.close(); + // Close the statement + createProducts.close(); + + Statement createOffers = connection.createStatement(); + createOffers.execute( + "CREATE TABLE IF NOT EXISTS offers (" + + "offerId TEXT, " + + "sourceWebshop TEXT, " + + "sourceId TEXT," + + "sourcePrice REAL, " + + "targetWebshop TEXT, " + + "targetId TEXT, " + + "targetPrice REAL, " + + "lastUpdate TEXT, " + + "UNIQUE(offerId) ON CONFLICT REPLACE" + + ")"); + createOffers.close(); + + Statement createTransactions = connection.createStatement(); + createTransactions.execute( + "CREATE TABLE IF NOT EXISTS transactions (" + + "offerId TEXT, " + + "earnings REAL, " + + "volume REAL, " + + "transactionTime TEXT" + + ")"); + createTransactions.close(); + + // Close the connection connection.close(); } catch (SQLException e) { throw new PersistenceException("Something went wrong while initializing SQLite DB", e); @@ -83,53 +133,64 @@ public final class SQLiteConnector extends AbstractProductPersistence { * * @param dto */ - public void saveProduct(final ProductDTO dto) { + public void writeProduct(final ProductDTO dto) { String query = "INSERT INTO products (" + "dataOrigin, productId, currentPrice, merchant, " - + "deliveryPrice, available, lastupdate" - + ") VALUES (" - + "?, ?, ?, ?, ?, ?, datetime('now', 'localtime')" + + "deliveryPrice, available, lastUpdate" + + ") VALUES ( " + + "?, ?, ?, ?, ?, ?, ?" + ")"; try (Connection connection = db.getConnection(); PreparedStatement statement = connection.prepareStatement(query)) { int index = 0; - statement.setString(++index, dto.getDataOrigin()); + statement.setString(++index, dto.getDataOrigin().toString()); 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.setString(++index, dto.getLastUpdate()); statement.executeUpdate(); } catch (SQLException e) { - throw new PersistenceException("Something went wrong while saving to SQLite", e); + throw new PersistenceException( + "Something went wrong while saving product to SQLite", e); } } - @Override + /** + * Gets a ProductDTO by identifier. + * + * @param productId + * @return DTO + */ 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)) { + 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")); + Webshop newShop = Webshop.fromString(resultSet.getString("dataOrigin")); + + dto = new ProductDTO(resultSet.getString("productId"), newShop); dto.setCurrentPrice(resultSet.getDouble("currentPrice")); dto.setMerchant(resultSet.getString("merchant")); dto.setDeliveryPrice(resultSet.getDouble("deliveryPrice")); dto.setAvailable(resultSet.getBoolean("available")); + dto.setLastUpdate(resultSet.getString(LAST_UPDATE_COL_NAME)); } } } catch (SQLException e) { - throw new PersistenceException("Something went wrong while reading from SQLite", e); + throw new PersistenceException( + "Something went wrong while reading product from SQLite"); } return dto; @@ -137,11 +198,201 @@ public final class SQLiteConnector extends AbstractProductPersistence { /** * Deletes all data from persistence. + * + * @throws SQLException */ public void clearData() { + clearProductData(); + clearOfferData(); + clearTXData(); + } + + /** + * Writes an offer to persistence. + * + * @param dto OfferDTO + */ + public void writeOffer(final OfferDTO dto) { + String query = "INSERT INTO offers (" + + "offerId, " + + "sourceWebshop, sourceId, sourcePrice, " + + "targetWebshop, targetId, targetPrice, " + + LAST_UPDATE_COL_NAME + + ") VALUES (" + + "?, ?, ?, ?, ?, ?, ?, ?" + + ")"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + int index = 0; + ProductDTO sourceProduct = dto.getSourceProduct(); + ProductDTO targetProduct = dto.getTargetProduct(); + + statement.setString(++index, dto.getOfferId()); + statement.setString(++index, sourceProduct.getDataOrigin().toString()); + statement.setString(++index, sourceProduct.getProductId()); + statement.setDouble(++index, sourceProduct.getCurrentPrice()); + statement.setString(++index, targetProduct.getDataOrigin().toString()); + statement.setString(++index, targetProduct.getProductId()); + statement.setDouble(++index, targetProduct.getCurrentPrice()); + statement.setString(++index, dto.getLastUpdate()); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while saving offer to SQLite", e); + } + } + + /** + * Gets an OfferDTO by identifier. + * + * @param offerId + * @return OfferDTO + */ + public OfferDTO getOfferDTOById(final String offerId) { + String query = "SELECT * FROM offers WHERE offerId = ?"; + OfferDTO dto = null; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + + statement.setString(1, offerId); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + dto = new OfferDTO(); + ProductDTO srcProduct = getProductDTOById(resultSet.getString("sourceId")); + ProductDTO targetProduct = getProductDTOById(resultSet.getString("targetId")); + dto.setOfferId(resultSet.getString(OFFERID_COL_NAME)); + dto.setSourceProduct(srcProduct); + dto.setTargetProduct(targetProduct); + dto.setLastUpdate(resultSet.getString(LAST_UPDATE_COL_NAME)); + } + } + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while reading offer from SQLite", e); + } + + return dto; + } + + @Override + public void writeTX(final TransactionDTO dto) { + String query = "INSERT INTO transactions (" + + "offerId, volume, earnings, transactionTime" + + ") VALUES (" + + "?, ?, ?, ?" + + ")"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + int index = 0; + + statement.setString(++index, dto.getOfferId()); + statement.setDouble(++index, dto.getVolume()); + statement.setDouble(++index, dto.getEarnings()); + statement.setString(++index, dto.getTransactionTime()); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while saving transaction to SQLite", e); + } + } + + @Override + public void outputTransactionsToLog() { + String query = "SELECT * FROM transactions t " + + "LEFT JOIN offers o ON t.offerId = o.offerId " + + "ORDER BY transactionTime ASC"; + StringBuilder sb = new StringBuilder(); + int index = 0; + sb.append("Transaction History:"); + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + sb.append("\n"); + sb.append(String.format("%3d", ++index)); + sb.append(String.format(" - %s - ", resultSet.getString("transactionTime"))); + sb.append(String.format("+%s (%s) - ", + FormattingUtil.formatEuro(resultSet.getDouble("earnings")), + FormattingUtil.formatEuro(resultSet.getDouble("volume")))); + sb.append(String.format("[%s to %s] %s", + resultSet.getString("sourceWebshop"), + resultSet.getString("targetWebshop"), + resultSet.getString(OFFERID_COL_NAME))); + } + } + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while reading transaction from SQLite", e); + } + + String logString = sb.toString(); + LOGGER.info(logString); + } + + @Override + public void outputSummaryToLog() { + String query = "SELECT SUM(earnings) AS earnings, SUM(volume) AS volume FROM transactions;"; + StringBuilder sb = new StringBuilder(); + sb.append("Summary:\n"); + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + double earnings = resultSet.getDouble("earnings"); + double volume = resultSet.getDouble("volume"); + String avgMargin = String.valueOf( + Math.round(earnings / volume * PERCENT)) + "%"; + + String earningsFormatted = FormattingUtil.formatEuro(earnings); + String volumeFormatted = FormattingUtil.formatEuro(volume); + + sb.append(String.format("Total earnings: %s, ", earningsFormatted)); + sb.append(String.format("total volume: %s ", volumeFormatted)); + sb.append(String.format("(average margin %s)", avgMargin)); + } + } + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while reading transaction summary from SQLite", e); + } + + String logString = sb.toString(); + LOGGER.info(logString); + } + + @Override + public void clearTXData() { + runDeletionQuery("DELETE FROM transactions;"); + } + + @Override + public void clearOfferData() { + runDeletionQuery("DELETE FROM offers;"); + } + + @Override + public void clearProductData() { + runDeletionQuery("DELETE FROM products;"); + } + + /** + * Flushes all data using the specified query. + * + * @param query + */ + private void runDeletionQuery(final String query) { 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); @@ -149,25 +400,46 @@ public final class SQLiteConnector extends AbstractProductPersistence { } @Override - protected String getDataOrigin() { - return DATA_ORIGIN; + public void deleteOfferById(final String offerId) { + String query = "DELETE FROM offers WHERE offerID = ?;"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + statement.setString(1, offerId); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while deleting offer from SQLite", e); + } } @Override - protected String getApiKey() { - throw new UnsupportedOperationException( - this.getClass().getName() + " doesn't support getApiKey"); - } + public List getOffers() { + List list = new ArrayList<>(); + String query = "SELECT * FROM offers"; - @Override - protected ProductDTO buildProductDTO(final ProductDTO product, final String json) { - throw new UnsupportedOperationException( - this.getClass().getName() + " doesn't support buildProductDTO"); - } + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { - @Override - protected URL createApiUrl(final String productIdentifier) { - throw new UnsupportedOperationException( - this.getClass().getName() + " doesn't support createApiUrl"); + try (ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + OfferDTO dto = new OfferDTO(); + ProductDTO srcProduct = getProductDTOById(resultSet.getString("sourceId")); + ProductDTO targetProduct = getProductDTOById(resultSet.getString("targetId")); + dto.setOfferId(resultSet.getString(OFFERID_COL_NAME)); + dto.setSourceProduct(srcProduct); + dto.setTargetProduct(targetProduct); + dto.setLastUpdate(resultSet.getString(LAST_UPDATE_COL_NAME)); + + list.add(dto); + } + } + } catch (SQLException e) { + throw new PersistenceException( + "Something went wrong while reading offers from SQLite", e); + } + + return list; } } diff --git a/src/main/java/de/rwu/easydrop/data/connector/TransactionPersistenceInterface.java b/src/main/java/de/rwu/easydrop/data/connector/TransactionPersistenceInterface.java new file mode 100644 index 0000000..8a930a3 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/TransactionPersistenceInterface.java @@ -0,0 +1,32 @@ +package de.rwu.easydrop.data.connector; + +import de.rwu.easydrop.api.dto.TransactionDTO; + +/** + * Persistence interface for transactions. + * + * @since 0.3.0 + */ +public interface TransactionPersistenceInterface { + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + void writeTX(TransactionDTO dto); + + /** + * Writes transactions to log. + */ + void outputTransactionsToLog(); + + /** + * Writes transaction summary (total volume and earnings) to log. + */ + void outputSummaryToLog(); + + /** + * Deletes all transaction data from persistence. + */ + void clearTXData(); +} diff --git a/src/main/java/de/rwu/easydrop/exception/DataWriterException.java b/src/main/java/de/rwu/easydrop/exception/DataWriterException.java new file mode 100644 index 0000000..6c1757e --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/DataWriterException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies a communication problem with a writer API. + * + * @since 0.3.0 + */ +public class DataWriterException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public DataWriterException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public DataWriterException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java b/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java new file mode 100644 index 0000000..69e582a --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/InvalidCatalogueException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies the data of a Product are invalid. + * + * @since 0.3.0 + */ +public class InvalidCatalogueException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public InvalidCatalogueException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public InvalidCatalogueException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java b/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java new file mode 100644 index 0000000..aefe676 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/InvalidOfferException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies the data of an Offer are invalid. + * + * @since 0.3.0 + */ +public class InvalidOfferException extends RuntimeException { + /** + * Throws an exception that signifies the data of an Offer are invalid. + * + * @param message + */ + public InvalidOfferException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of an Offer are invalid. + * + * @param message + * @param cause + */ + public InvalidOfferException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidTransactionException.java b/src/main/java/de/rwu/easydrop/exception/InvalidTransactionException.java new file mode 100644 index 0000000..66442a4 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/InvalidTransactionException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies the data of a Transaction are invalid. + * + * @since 0.3.0 + */ +public class InvalidTransactionException extends RuntimeException { + /** + * Throws an exception that signifies the data of an Offer are invalid. + * + * @param message + */ + public InvalidTransactionException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of an Offer are invalid. + * + * @param message + * @param cause + */ + public InvalidTransactionException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/model/Offer.java b/src/main/java/de/rwu/easydrop/model/Offer.java new file mode 100644 index 0000000..b2e3577 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/Offer.java @@ -0,0 +1,31 @@ +package de.rwu.easydrop.model; + +import lombok.Data; + +/** + * An Offer. + * + * @since 0.3.0 + */ +@Data +public class Offer { + /** + * ID of the offer, built from identifiers of the source platforms. + */ + private String offerId; + + /** + * The product that our software buys. + */ + private Product sourceProduct; + + /** + * The product that our software sells. + */ + private Product targetProduct; + + /** + * Date of last update of the offer. + */ + private String lastUpdate; +} diff --git a/src/main/java/de/rwu/easydrop/model/Product.java b/src/main/java/de/rwu/easydrop/model/Product.java index 1e5b3b2..dd412dd 100644 --- a/src/main/java/de/rwu/easydrop/model/Product.java +++ b/src/main/java/de/rwu/easydrop/model/Product.java @@ -13,7 +13,7 @@ public class Product { /** * Data source platform, like "Amazon". */ - private String dataOrigin; + private Webshop dataOrigin; /** * Platform internal product identifier. @@ -40,6 +40,11 @@ public class Product { */ private boolean available; + /** + * Last update from API. + */ + private String lastUpdate; + @Override public final String toString() { return "Product: [" diff --git a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java index 97f31dc..e907bcc 100644 --- a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java +++ b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java @@ -68,11 +68,11 @@ public class ProductCatalogue { @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(String.format("Catalogue Name: %s%n", productName)); + sb.append(String.format("Description: %s%n", description)); sb.append("Products:\n"); for (Product product : products) { - sb.append(product.toString()).append("\n"); + sb.append(String.format("%s%n", product.toString())); } return sb.toString(); } diff --git a/src/main/java/de/rwu/easydrop/model/ProductPair.java b/src/main/java/de/rwu/easydrop/model/ProductPair.java new file mode 100644 index 0000000..70140c0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/ProductPair.java @@ -0,0 +1,31 @@ +package de.rwu.easydrop.model; + +import lombok.Data; + +/** + * Associates two related Products to one another. + * + * @since 0.3.0 + */ +@Data +public class ProductPair { + /** + * The first product. + */ + private final Product product1; + /** + * The second product. + */ + private final Product product2; + + /** + * Constructs a Product Pair. + * + * @param a First product + * @param b Second product + */ + public ProductPair(final Product a, final Product b) { + this.product1 = a; + this.product2 = b; + } +} diff --git a/src/main/java/de/rwu/easydrop/model/Transaction.java b/src/main/java/de/rwu/easydrop/model/Transaction.java new file mode 100644 index 0000000..a8236fb --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/Transaction.java @@ -0,0 +1,30 @@ +package de.rwu.easydrop.model; + +import lombok.Data; + +/** + * A transaction. + * + * @since 0.3.0 + */ +@Data +public class Transaction { + /** + * Offer ID. + * + * @see Offer + */ + private String offerId; + /** + * Sales volume. + */ + private double volume; + /** + * Earnings (volume - cost). + */ + private double earnings; + /** + * Transaction timestamp. + */ + private String transactionTime; +} diff --git a/src/main/java/de/rwu/easydrop/model/Webshop.java b/src/main/java/de/rwu/easydrop/model/Webshop.java new file mode 100644 index 0000000..bab281e --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/Webshop.java @@ -0,0 +1,31 @@ +package de.rwu.easydrop.model; + +/** + * Constants for data source/destination platforms. + */ +public enum Webshop { + /** + * Amazon Product API. + */ + AMAZON, + /** + * eBay Item API. + */ + EBAY; + + /** + * Attempts to derive a webshop value from a string. + * + * @param str String + * @return webshop + * @throws IllegalArgumentException + */ + public static Webshop fromString(final String str) { + for (Webshop shop : Webshop.values()) { + if (shop.toString().equalsIgnoreCase(str)) { + return shop; + } + } + throw new IllegalArgumentException(String.format("No webshop called %s found", str)); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java new file mode 100644 index 0000000..aa98afe --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/mapping/OfferMapper.java @@ -0,0 +1,56 @@ +package de.rwu.easydrop.service.mapping; + +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.model.Offer; + +/** + * Maps between Offer, OfferDTO and OfferDTO. + * + * @since 0.2.0 + * + * @see Offer + * @see OfferDTO + */ +public final class OfferMapper { + + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private OfferMapper() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a mapping class, don't instantiate it."); + } + + /** + * Creates a Offer object from a corresponding DTO. + * + * @param dto Offer Data Transfer Object + * @return Offer + */ + public static Offer mapOfferFromDTO(final OfferDTO dto) { + Offer offer = new Offer(); + offer.setOfferId(dto.getOfferId()); + offer.setSourceProduct(ProductMapper.mapProductFromDTO(dto.getSourceProduct())); + offer.setTargetProduct(ProductMapper.mapProductFromDTO(dto.getTargetProduct())); + offer.setLastUpdate(dto.getLastUpdate()); + + return offer; + } + + /** + * Creates an OfferDTO object from a corresponding offer. + * + * @param offer + * @return OfferDTO + */ + public static OfferDTO mapOfferToDTO(final Offer offer) { + OfferDTO dto = new OfferDTO(); + dto.setOfferId(offer.getOfferId()); + dto.setSourceProduct(ProductMapper.mapProductToDTO(offer.getSourceProduct())); + dto.setTargetProduct(ProductMapper.mapProductToDTO(offer.getTargetProduct())); + dto.setLastUpdate(offer.getLastUpdate()); + + return dto; + } +} 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 123080a..6ada1b4 100644 --- a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java +++ b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java @@ -39,6 +39,7 @@ public final class ProductMapper { product.setDeliveryPrice(dto.getDeliveryPrice()); product.setMerchant(dto.getMerchant()); product.setProductId(dto.getProductId()); + product.setLastUpdate(dto.getLastUpdate()); return product; } @@ -56,6 +57,7 @@ public final class ProductMapper { dto.setCurrentPrice(product.getCurrentPrice()); dto.setDeliveryPrice(product.getDeliveryPrice()); dto.setMerchant(product.getMerchant()); + dto.setLastUpdate(product.getLastUpdate()); return dto; } diff --git a/src/main/java/de/rwu/easydrop/service/mapping/TransactionMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/TransactionMapper.java new file mode 100644 index 0000000..47c68f5 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/mapping/TransactionMapper.java @@ -0,0 +1,54 @@ +package de.rwu.easydrop.service.mapping; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.model.Transaction; + +/** + * Maps transaction DTOs and objects. + * + * @since 0.3.0 + */ +public final class TransactionMapper { + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private TransactionMapper() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a mapping class, don't instantiate it."); + } + + /** + * Creates a Transaction object from a corresponding DTO. + * + * @param dto Transaction Data Transfer Object + * @return Product + */ + public static Transaction mapTXFromDTO(final TransactionDTO dto) { + Transaction tx = new Transaction(); + + tx.setOfferId(dto.getOfferId()); + tx.setVolume(dto.getVolume()); + tx.setEarnings(dto.getEarnings()); + tx.setTransactionTime(dto.getTransactionTime()); + + return tx; + } + + /** + * Creates a ProductDTO object from a corresponding Product. + * + * @param tx Transaction + * @return TransactionDTO + */ + public static TransactionDTO mapTXToDTO(final Transaction tx) { + TransactionDTO dto = new TransactionDTO(); + + dto.setOfferId(tx.getOfferId()); + dto.setVolume(tx.getVolume()); + dto.setEarnings(tx.getEarnings()); + dto.setTransactionTime(tx.getTransactionTime()); + + return dto; + } +} diff --git a/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java b/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java new file mode 100644 index 0000000..52e8357 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/processing/OfferIdentifier.java @@ -0,0 +1,91 @@ +package de.rwu.easydrop.service.processing; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.rwu.easydrop.exception.InvalidCatalogueException; +import de.rwu.easydrop.exception.InvalidOfferException; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.ProductPair; +import de.rwu.easydrop.util.FormattingUtil; +import de.rwu.easydrop.util.Timestamp; + +public class OfferIdentifier { + /** + * Logging instance. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(OfferIdentifier.class); + + /** + * runIdentifier calls the price function that decides + * if it is feasible to dropship products. + * + * @param pCats Product catalogues + * @return Identified offers + */ + public List runIdentifier(final List pCats) { + List identifiedOffers = new ArrayList<>(); + for (ProductCatalogue pCat : pCats) { + + // Call price finder for all catalogue + try { + ProductPair pair = getHighestMarginProducts(pCat); + Offer possibleOffer = new Offer(); + possibleOffer.setLastUpdate(Timestamp.now()); + possibleOffer.setSourceProduct(pair.getProduct1()); + possibleOffer.setTargetProduct(pair.getProduct2()); + identifiedOffers.add(possibleOffer); + + double margin = pair.getProduct2().getCurrentPrice() + - pair.getProduct1().getCurrentPrice(); + String marginFormatted = FormattingUtil.formatEuro(margin); + LOGGER.info("\n Identified Offer: {} ({} to {}) with margin {} ", + pCat.getProductName(), + pair.getProduct1().getDataOrigin(), + pair.getProduct2().getDataOrigin(), + marginFormatted); + } catch (InvalidOfferException e) { + // Don't include offer if deemed invalid + } + } + + return identifiedOffers; + } + + /** + * Returns the cheapest and most expensive product of a catalogue to guarantee + * the biggest margin as a pair. + * + * @param pCat Product Catalogue + * @return Cheapest Product, Most Expensive Product as a Product Pair + */ + public static ProductPair getHighestMarginProducts(final ProductCatalogue pCat) { + if (pCat.getProducts().size() < 2) { + throw new InvalidCatalogueException("Product Catalogue holds less than 2 products!"); + } + + // Initialize indexes + Product cheapestProduct = pCat.getProducts().get(0); + Product mostExpensiveProduct = pCat.getProducts().get(0); + + for (Product product : pCat.getProducts()) { + if (product.getCurrentPrice() < cheapestProduct.getCurrentPrice()) { + cheapestProduct = product; + } + if (product.getCurrentPrice() > mostExpensiveProduct.getCurrentPrice()) { + mostExpensiveProduct = product; + } + } + + if (cheapestProduct.getCurrentPrice() == mostExpensiveProduct.getCurrentPrice()) { + throw new InvalidOfferException("Price margin is zero!"); + } + + return new ProductPair(cheapestProduct, mostExpensiveProduct); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java new file mode 100644 index 0000000..de6e4eb --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/processing/OfferProvisioner.java @@ -0,0 +1,115 @@ +package de.rwu.easydrop.service.processing; + +import java.util.List; + +import de.rwu.easydrop.api.client.AmazonPurchaser; +import de.rwu.easydrop.api.client.AmazonSeller; +import de.rwu.easydrop.api.client.EbayPurchaser; +import de.rwu.easydrop.api.client.EbaySeller; +import de.rwu.easydrop.api.client.PurchaserFactory; +import de.rwu.easydrop.api.client.SellerFactory; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.service.mapping.ProductMapper; +import de.rwu.easydrop.service.writer.OfferWriter; +import de.rwu.easydrop.util.FormattingUtil; +import lombok.Data; + +/** + * Offer provisioner. + * + * @since 0.3.0 + */ +@Data +public class OfferProvisioner { + /** + * Writes offers into persistence. + */ + private OfferWriter offerWriter; + + /** + * Is the API for selling products on Amazon. + */ + private AmazonSeller amazonSeller; + /** + * Is the API for selling products on Ebay. + */ + private EbaySeller ebaySeller; + /** + * Is the API for buying products on Amazon. + */ + private AmazonPurchaser amazonPurchaser; + /** + * Is the API for buying products on Ebay. + */ + private EbayPurchaser ebayPurchaser; + + /** + * Puts up the sell order. + * + * @param offer + */ + private void toSeller(final Offer offer) throws IllegalArgumentException { + switch (offer.getTargetProduct().getDataOrigin()) { + case EBAY -> this.ebaySeller.sellProduct( + ProductMapper.mapProductToDTO(offer.getTargetProduct())); + case AMAZON -> this.amazonSeller.sellProduct( + ProductMapper.mapProductToDTO(offer.getTargetProduct())); + default -> throw new IllegalArgumentException("Unsupported source plattform"); + } + } + + /** + * Puts up the buy order. + * + * @param offer + */ + private void toBuyer(final Offer offer) throws IllegalArgumentException { + switch (offer.getSourceProduct().getDataOrigin()) { + case EBAY -> ebayPurchaser.purchaseProduct( + ProductMapper.mapProductToDTO(offer.getSourceProduct())); + case AMAZON -> amazonPurchaser.purchaseProduct( + ProductMapper.mapProductToDTO(offer.getSourceProduct())); + default -> throw new IllegalArgumentException("Unsupported target plattform"); + } + } + + /** + * Is the class for placing orders on a target platform. + * + * @param db Persistence Interface + * @param newSellerFactory Seller Factory + * @param newPurchaserFactory Purchaser Factory + */ + public OfferProvisioner( + final OfferPersistenceInterface db, + final SellerFactory newSellerFactory, + final PurchaserFactory newPurchaserFactory) { + this.offerWriter = new OfferWriter(db); + this.ebaySeller = newSellerFactory.createEbaySeller(); + this.amazonSeller = newSellerFactory.createAmazonSeller(); + this.ebayPurchaser = newPurchaserFactory.createEbayPurchaser(); + this.amazonPurchaser = newPurchaserFactory.createAmazonPurchaser(); + } + + /** + * Method for placing orders on a target platform. + * + * @param offersToProvision + */ + public final void runProvisioner(final List offersToProvision) { + for (Offer newOffer : offersToProvision) { + String newOfferId = FormattingUtil.removeSpaces( + newOffer.getSourceProduct().getDataOrigin().toString() + + newOffer.getTargetProduct().getDataOrigin().toString() + + "_" + + newOffer.getSourceProduct().getProductId() + + newOffer.getTargetProduct().getProductId()); + + this.toSeller(newOffer); + this.toBuyer(newOffer); + newOffer.setOfferId(newOfferId); + offerWriter.writeOfferToPersistence(newOffer); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java deleted file mode 100644 index 4d373b2..0000000 --- a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.service.processing; - -/** - * Processes dropshipping orders. - * - * TODO implement - */ -public class OrderManager { - -} diff --git a/src/main/java/de/rwu/easydrop/service/processing/TransactionHandler.java b/src/main/java/de/rwu/easydrop/service/processing/TransactionHandler.java new file mode 100644 index 0000000..928625c --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/processing/TransactionHandler.java @@ -0,0 +1,47 @@ +package de.rwu.easydrop.service.processing; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Transaction; +import de.rwu.easydrop.service.mapping.TransactionMapper; +import de.rwu.easydrop.util.Timestamp; + +/** + * Handles transactions. + * + * @since 0.3.0 + */ +public class TransactionHandler { + /** + * Transaction persistence. + */ + private TransactionPersistenceInterface txPersistence; + + /** + * Creates new transaction handler. + * + * @param txdb transaction persistence + */ + public TransactionHandler(final TransactionPersistenceInterface txdb) { + this.txPersistence = txdb; + } + + /** + * Creates transaction from offer. + * + * @param offer Offer + */ + public void turnOfferToTransaction(final Offer offer) { + Transaction tx = new Transaction(); + tx.setOfferId(offer.getOfferId()); + tx.setVolume(offer.getTargetProduct().getCurrentPrice()); + tx.setEarnings(offer.getTargetProduct().getCurrentPrice() + - offer.getSourceProduct().getCurrentPrice()); + tx.setTransactionTime(Timestamp.now()); + + // Write transaction to persistence + TransactionDTO txDTO = TransactionMapper.mapTXToDTO(tx); + txPersistence.writeTX(txDTO); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java index 64bb8c0..44c32a3 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java @@ -5,7 +5,9 @@ import java.util.List; import javax.naming.ConfigurationException; -import de.rwu.easydrop.exception.InvalidProductException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.util.ProductsConfig; @@ -18,6 +20,11 @@ import lombok.Data; */ @Data public class CatalogueRetriever { + /** + * Logging instance. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(CatalogueRetriever.class); + /** * User-configured products. */ @@ -57,22 +64,16 @@ public class CatalogueRetriever { 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"); - } + Product newProduct = productRetriever.getProductFromWebshop(product.getDataOrigin(), + product.getProductId()); newProductCatalogue.addProduct(newProduct); } productCatalogues.add(newProductCatalogue); + String catString = String.format( + "%nLoaded Catalogue: %n%s", newProductCatalogue.toString()); + LOGGER.info(catString); } } } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java new file mode 100644 index 0000000..4cef58d --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/retriever/OfferRetriever.java @@ -0,0 +1,40 @@ +package de.rwu.easydrop.service.retriever; + +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.service.mapping.OfferMapper; + +/** + * Retrieves offer information from different sources. + * + * @since 0.3.0 + */ +public class OfferRetriever { + /** + * Persistence interface. + */ + private OfferPersistenceInterface persistence; + + /** + * Creates an Offer Retriever. + * + * @param db Persistence Interface + */ + public OfferRetriever(final OfferPersistenceInterface db) { + this.persistence = db; + } + + /** + * Retrieves an offer from persistence. + * + * @param offerId + * @return Offer from persistence + */ + public Offer getOfferFromPersistence(final String offerId) { + OfferPersistenceInterface src = persistence; + + OfferDTO dto = src.getOfferDTOById(offerId); + return OfferMapper.mapOfferFromDTO(dto); + } +} 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 da842c3..e2b4d5f 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -4,8 +4,9 @@ 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.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.validation.ProductValidator; @@ -19,19 +20,20 @@ public class ProductRetriever { * Data source factory. */ private DataSourceFactory dataSourceFactory; - /** - * @param newDataSourceFactory the dataSourceFactory to set + * Persistence interface. */ - public void setDataSourceFactory(final DataSourceFactory newDataSourceFactory) { - this.dataSourceFactory = newDataSourceFactory; - } + private ProductPersistenceInterface persistence; /** * @param newDataSourceFactory + * @param newPersistence */ - public ProductRetriever(final DataSourceFactory newDataSourceFactory) { - this.setDataSourceFactory(newDataSourceFactory); + public ProductRetriever( + final DataSourceFactory newDataSourceFactory, + final ProductPersistenceInterface newPersistence) { + this.dataSourceFactory = newDataSourceFactory; + this.persistence = newPersistence; } /** @@ -73,12 +75,27 @@ public class ProductRetriever { * @return Product from persistence */ public Product getProductFromPersistence(final String productId) { - AbstractProductPersistence src = dataSourceFactory.createProductPersistenceDataSource(); - - ProductDTO dto = src.getProductDTOById(productId); + ProductDTO dto = persistence.getProductDTOById(productId); Product product = ProductMapper.mapProductFromDTO(dto); ProductValidator.validate(product); return product; } + + /** + * Retrieves a product from an API by name of the API so that high-level + * components do not need to be extended + * with exact function call names if we extend the list of webshops. + * + * @param shop Data source name, e.g. Amazon + * @param productIdentifier Product name, translated to the correct product ID + * for the data source + * @return Product from that data source or null if data source not available + */ + public Product getProductFromWebshop(final Webshop shop, final String productIdentifier) { + return switch (shop) { + case AMAZON -> getProductFromAmazon(productIdentifier); + case EBAY -> getProductFromEbay(productIdentifier); + }; + } } diff --git a/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java new file mode 100644 index 0000000..edf5491 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/validation/OfferValidator.java @@ -0,0 +1,38 @@ +package de.rwu.easydrop.service.validation; + +import de.rwu.easydrop.exception.InvalidOfferException; +import de.rwu.easydrop.model.Offer; + +/** + * Confirms validity of Offer data. + * + * @since 0.2.0 + */ +public final class OfferValidator { + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private OfferValidator() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a validator class, don't instantiate it."); + } + + /** + * Makes sure an Offer does not contain invalid information. + * + * @param offer the Offer + */ + public static void validate(final Offer offer) { + try { + if (offer.getOfferId().equals("")) { + throw new InvalidOfferException("Offer ID cannot be empty"); + } + if (offer.getLastUpdate().equals("")) { + throw new InvalidOfferException("LastUpdate cannot be empty"); + } + } catch (NullPointerException e) { + throw new InvalidOfferException("Required information is missing in the offer", e); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java index fc48377..f74f054 100644 --- a/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java +++ b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java @@ -1,8 +1,5 @@ package de.rwu.easydrop.service.validation; -import java.util.HashSet; -import java.util.Set; - import de.rwu.easydrop.exception.InvalidProductException; import de.rwu.easydrop.model.Product; @@ -31,26 +28,8 @@ public final class ProductValidator { if (product.getCurrentPrice() == 0.00) { throw new InvalidProductException("Current price cannot be 0.00"); } - if (!isInValidDataOrigins(product.getDataOrigin())) { - throw new InvalidProductException("Unknown data source"); - } if (product.getProductId().equals("")) { throw new InvalidProductException("Product ID cannot be empty"); } } - - /** - * Checks whether a dataOrigin is within the set of valid ones. - * - * @param dataOrigin like "Amazon" - * @return true if valid - */ - public static boolean isInValidDataOrigins(final String dataOrigin) { - Set validOrigins = new HashSet<>(); - - validOrigins.add("Amazon"); - validOrigins.add("eBay"); - - return validOrigins.contains(dataOrigin); - } } diff --git a/src/main/java/de/rwu/easydrop/service/validation/TransactionValidator.java b/src/main/java/de/rwu/easydrop/service/validation/TransactionValidator.java new file mode 100644 index 0000000..b73e435 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/validation/TransactionValidator.java @@ -0,0 +1,42 @@ +package de.rwu.easydrop.service.validation; + +import de.rwu.easydrop.exception.InvalidTransactionException; +import de.rwu.easydrop.model.Transaction; + +/** + * Validates transactions. + * + * @since 0.3.0 + */ +public final class TransactionValidator { + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private TransactionValidator() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a validator class, don't instantiate it."); + } + + /** + * Makes sure a transaction does not contain invalid information. + * + * @param tx the Transaction + */ + public static void validate(final Transaction tx) { + try { + if (tx.getOfferId().equals("")) { + throw new InvalidTransactionException("Offer ID cannot be empty"); + } + if (tx.getEarnings() == 0) { + throw new InvalidTransactionException("Earnings can't be zero"); + } + if (tx.getVolume() == 0) { + throw new InvalidTransactionException("Volume can't be zero"); + } + } catch (NullPointerException e) { + throw new InvalidTransactionException( + "Required information is missing in the transaction", e); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java b/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java index a1cf486..c243f60 100644 --- a/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java +++ b/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java @@ -3,7 +3,7 @@ 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.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.service.mapping.ProductMapper; @@ -18,14 +18,14 @@ public final class CatalogueWriter { /** * Holds a persistence reference. */ - private AbstractProductPersistence persistence; + private ProductPersistenceInterface persistence; /** * Creates new instance. * * @param newPersistence */ - public CatalogueWriter(final AbstractProductPersistence newPersistence) { + public CatalogueWriter(final ProductPersistenceInterface newPersistence) { persistence = newPersistence; } @@ -40,7 +40,7 @@ public final class CatalogueWriter { ProductValidator.validate(product); ProductDTO dto = ProductMapper.mapProductToDTO(product); - persistence.saveProduct(dto); + persistence.writeProduct(dto); } } } diff --git a/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java b/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java new file mode 100644 index 0000000..ba43912 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/OfferWriter.java @@ -0,0 +1,31 @@ +package de.rwu.easydrop.service.writer; + +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.service.mapping.OfferMapper; + +public class OfferWriter { + /** + * Persistence. + */ + private OfferPersistenceInterface persistence; + + /** + * @param newPersistence the persistence to set + */ + public OfferWriter(final OfferPersistenceInterface newPersistence) { + this.persistence = newPersistence; + } + + /** + * Validates and saves product to persistence. + * + * @param offer + */ + public void writeOfferToPersistence(final Offer offer) { + OfferDTO dto = OfferMapper.mapOfferToDTO(offer); + + persistence.writeOffer(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 index d1be850..79d8114 100644 --- a/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java +++ b/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java @@ -1,7 +1,7 @@ package de.rwu.easydrop.service.writer; import de.rwu.easydrop.api.dto.ProductDTO; -import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.validation.ProductValidator; @@ -15,12 +15,12 @@ public class ProductWriter { /** * Persistence. */ - private AbstractProductPersistence persistence; + private ProductPersistenceInterface persistence; /** * @param newPersistence the persistence to set */ - public void setPersistence(final AbstractProductPersistence newPersistence) { + public ProductWriter(final ProductPersistenceInterface newPersistence) { this.persistence = newPersistence; } @@ -33,6 +33,6 @@ public class ProductWriter { ProductValidator.validate(product); ProductDTO dto = ProductMapper.mapProductToDTO(product); - persistence.saveProduct(dto); + persistence.writeProduct(dto); } } diff --git a/src/main/java/de/rwu/easydrop/service/writer/TransactionWriter.java b/src/main/java/de/rwu/easydrop/service/writer/TransactionWriter.java new file mode 100644 index 0000000..ff649c0 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/TransactionWriter.java @@ -0,0 +1,33 @@ +package de.rwu.easydrop.service.writer; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.model.Transaction; +import de.rwu.easydrop.service.mapping.TransactionMapper; +import de.rwu.easydrop.service.validation.TransactionValidator; + +public class TransactionWriter { + /** + * Persistence. + */ + private TransactionPersistenceInterface persistence; + + /** + * @param newPersistence the persistence to set + */ + public TransactionWriter(final TransactionPersistenceInterface newPersistence) { + this.persistence = newPersistence; + } + + /** + * Validates and saves a transaction to persistence. + * + * @param tx Transaction + */ + public void writeTXToPersistence(final Transaction tx) { + TransactionDTO dto = TransactionMapper.mapTXToDTO(tx); + TransactionValidator.validate(tx); + + persistence.writeTX(dto); + } +} diff --git a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java index 7ef0e69..544e729 100644 --- a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java +++ b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java @@ -25,7 +25,7 @@ public final class FormattingUtil { * @return formatted price */ public static String formatEuro(final double amount) { - return String.format(Locale.GERMAN, "%,.2f", amount) + " €"; + return String.format(Locale.GERMAN, "%,.2f", amount) + " Euro"; } /** @@ -37,4 +37,14 @@ public final class FormattingUtil { public static String urlEncode(final String str) { return str.replace(" ", "+"); } + + /** + * Removes spaces from target string. + * + * @param str String + * @return Space-less string + */ + public static String removeSpaces(final String str) { + return str.replace(" ", ""); + } } diff --git a/src/main/java/de/rwu/easydrop/util/ProductsConfig.java b/src/main/java/de/rwu/easydrop/util/ProductsConfig.java index b69777f..2295a45 100644 --- a/src/main/java/de/rwu/easydrop/util/ProductsConfig.java +++ b/src/main/java/de/rwu/easydrop/util/ProductsConfig.java @@ -13,6 +13,7 @@ import com.jayway.jsonpath.JsonPathException; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.Webshop; /** * Reads the user-specified catalogue of products. @@ -122,8 +123,8 @@ public final class ProductsConfig { ArrayList> identifiers = JsonPath.read(jsonIdentifiers, "$"); for (HashMap product : identifiers) { - String dataOrigin = product.keySet().iterator().next(); - String identifier = product.get(dataOrigin).toString(); + Webshop dataOrigin = Webshop.fromString(product.keySet().iterator().next()); + String identifier = product.get(dataOrigin.toString()).toString(); Product newProduct = new Product(); newProduct.setDataOrigin(dataOrigin); diff --git a/src/main/java/de/rwu/easydrop/util/Timestamp.java b/src/main/java/de/rwu/easydrop/util/Timestamp.java new file mode 100644 index 0000000..1532c96 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/util/Timestamp.java @@ -0,0 +1,29 @@ +package de.rwu.easydrop.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Serves as a timestamp source. + * + * @since 0.3.0 + */ +public final class Timestamp { + /** + * Hidden constructor. + */ + private Timestamp() { + // Don't instantiate me! + } + + /** + * Returns a formatted time string. + * + * @return String such as "2023-01-01 00:00:00" + */ + public static String now() { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return now.format(formatter); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java new file mode 100644 index 0000000..f7a2443 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractDataWriterTest.java @@ -0,0 +1,122 @@ +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.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +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.exception.DataWriterException; +import de.rwu.easydrop.model.Webshop; + +class AbstractDataWriterTest { + private static String demoProductId = "whateverId"; + + @Mock + private HttpURLConnection mockConnection; + @Mock + private URL mockUrl; + + private AbstractDataWriter writer; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + writer = new AbstractDataWriter() { + @Override + protected String getApiKey() { + return "testApiKey"; + } + + @Override + protected Webshop getDataTarget() { + return Webshop.AMAZON; + } + + @Override + protected URL createApiUrl(String productIdentifier) throws MalformedURLException { + return mockUrl; + } + }; + } + + @Test + void sendPutRequest_badResponseCode_throwsException() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO(demoProductId, Webshop.AMAZON); + + // Set up Mocks + AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); + doCallRealMethod().when(mockWriter).sendPutRequest(any(), anyString()); + when(mockUrl.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_BAD_REQUEST); + + DataWriterException e = assertThrows(DataWriterException.class, () -> { + writer.sendPutRequest(dto, "Sales"); + }); + + assertEquals("AMAZON Sales API responded with error code 400", e.getMessage()); + } + + @Test + void sendPutRequest_ioException_throwsException() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO(demoProductId, Webshop.AMAZON); + + // Set up Mocks + AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); + doCallRealMethod().when(mockWriter).sendPutRequest(any(), anyString()); + when(mockUrl.openConnection()).thenThrow(new IOException()); + + DataWriterException e = assertThrows(DataWriterException.class, () -> { + writer.sendPutRequest(dto, "testApiType"); + }); + + assertEquals("Couldn't fulfill AMAZON API request", e.getMessage()); + } + + @Test + void sendPutRequest_successfulRequest() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO(demoProductId, Webshop.AMAZON); + + // Set up Mocks + AbstractDataWriter mockWriter = mock(AbstractDataWriter.class); + URL mockURL = mock(URL.class); + when(mockWriter.createApiUrl(demoProductId)).thenReturn(mockURL); + doCallRealMethod().when(mockWriter).sendPutRequest(any(), anyString()); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockWriter.getDataTarget()).thenReturn(Webshop.AMAZON); + + assertDoesNotThrow(() -> { + mockWriter.sendPutRequest(dto, "Purchase"); + }); + } + + @Test + void sendPutRequest_wrongDataTarget() { + ProductDTO dto = new ProductDTO(demoProductId, Webshop.EBAY); + + DataWriterException e = assertThrows(DataWriterException.class, () -> { + writer.sendPutRequest(dto, demoProductId); + }); + + assertEquals("Product data source and target whateverId API are incompatible.", e.getMessage()); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java new file mode 100644 index 0000000..3bc169a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractPurchaserTest.java @@ -0,0 +1,39 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Webshop; + +class AbstractPurchaserTest { + private AbstractPurchaser mockPurchaser = mock(AbstractPurchaser.class); + + @Test + void purchaseProduct_CorrectApiTypeInException() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO("12345", Webshop.AMAZON); + + // Set up mocks + URL mockURL = mock(URL.class); + when(mockPurchaser.createApiUrl("12345")).thenReturn(mockURL); + doCallRealMethod().when(mockPurchaser).purchaseProduct(any()); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockPurchaser.getDataTarget()).thenReturn(Webshop.AMAZON); + + assertDoesNotThrow(() -> { + mockPurchaser.purchaseProduct(dto); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java b/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java new file mode 100644 index 0000000..aa3c299 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AbstractSellerTest.java @@ -0,0 +1,39 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Webshop; + +class AbstractSellerTest { + private AbstractSeller mockSeller = mock(AbstractSeller.class); + + @Test + void purchaseProduct_CorrectApiTypeInException() throws IOException { + // Set up DTO + ProductDTO dto = new ProductDTO("12345", Webshop.AMAZON); + + // Set up mocks + URL mockURL = mock(URL.class); + when(mockSeller.createApiUrl("12345")).thenReturn(mockURL); + doCallRealMethod().when(mockSeller).sellProduct(any()); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockSeller.getDataTarget()).thenReturn(Webshop.AMAZON); + + assertDoesNotThrow(() -> { + mockSeller.sellProduct(dto); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java index 938413d..14667b3 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java @@ -21,6 +21,7 @@ import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.DataSourceException; +import de.rwu.easydrop.model.Webshop; class AmazonProductDataSourceTest { @@ -28,7 +29,7 @@ class AmazonProductDataSourceTest { private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static String demoDataOrigin = "Amazon"; + private static Webshop demoDataOrigin = Webshop.AMAZON; private static String demoProductId = "whateverId"; @BeforeEach @@ -138,7 +139,7 @@ class AmazonProductDataSourceTest { // Verify the product DTO properties assertEquals(demoProductId, result.getProductId()); - assertEquals("Amazon", result.getDataOrigin()); + assertEquals(Webshop.AMAZON, result.getDataOrigin()); assertEquals(true, result.isAvailable()); assertEquals(10.0, result.getCurrentPrice()); assertEquals(2.5, result.getDeliveryPrice()); @@ -149,33 +150,60 @@ class AmazonProductDataSourceTest { void testGetProductDTOById_failedRequest() throws IOException { // Set up the test environment - AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + AmazonProductDataSource DataSource = mock(AmazonProductDataSource.class); URL mockURL = mock(URL.class); - when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin); - when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); - when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); + when(DataSource.getDataOrigin()).thenReturn(demoDataOrigin); + when(DataSource.createApiUrl(demoProductId)).thenReturn(mockURL); + when(DataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); // Invoke the method and verify the exception DataSourceException exception = assertThrows(DataSourceException.class, () -> { - dataSource.getProductDTOById(demoProductId); + DataSource.getProductDTOById(demoProductId); }); // Verify the exception message - assertEquals("Nothing found: Amazon API responded with error code 404", exception.getMessage()); + assertEquals("Nothing found: AMAZON API responded with error code 404", exception.getMessage()); + } + + @Test + void testGetProductDTOById_emptyResponse() throws IOException { + // Set up the test environment + String mockResponse = ""; + + AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + URL mockURL = mock(URL.class); + when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); + when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod(); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockConnection.getInputStream()).thenReturn(new ByteArrayInputStream(mockResponse.getBytes())); + when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); + + // Invoke the method + ProductDTO result = dataSource.getProductDTOById(demoProductId); + + // Verify the product DTO properties + assertEquals(demoProductId, result.getProductId()); + assertEquals(Webshop.AMAZON, result.getDataOrigin()); + assertEquals(false, result.isAvailable()); + assertEquals(0.0, result.getCurrentPrice()); + assertEquals(0.0, result.getDeliveryPrice()); + assertEquals(null, result.getMerchant()); } @Test void testGetProductDTOById_ioException() throws IOException { // Set up the test environment - AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + AmazonProductDataSource DataSource = mock(AmazonProductDataSource.class); URL mockURL = mock(URL.class); - when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin); - when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); - when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); - when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod(); + when(DataSource.getDataOrigin()).thenReturn(demoDataOrigin); + when(DataSource.createApiUrl(demoProductId)).thenReturn(mockURL); + when(DataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); + when(DataSource.buildProductDTO(any(), anyString())).thenCallRealMethod(); HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); @@ -183,16 +211,16 @@ class AmazonProductDataSourceTest { // Invoke the method and verify the exception DataSourceException exception = assertThrows(DataSourceException.class, () -> { - dataSource.getProductDTOById(demoProductId); + DataSource.getProductDTOById(demoProductId); }); // Verify the exception message - assertEquals("Couldn't fulfill Amazon API request", exception.getMessage()); + assertEquals("Couldn't fulfill AMAZON API request", exception.getMessage()); } @Test void getDataOrigin_ReturnsExpectedDataOrigin() { - String dataOrigin = demoDataSource.getDataOrigin(); + Webshop dataOrigin = demoDataSource.getDataOrigin(); assertEquals(demoDataOrigin, dataOrigin); } diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java new file mode 100644 index 0000000..788fe5e --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonPurchaserTest.java @@ -0,0 +1,82 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.model.Webshop; + +class AmazonPurchaserTest { + private AmazonPurchaser demoPurchaser; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static Webshop demoDataTarget = Webshop.AMAZON; + + @BeforeEach + void setup() { + demoPurchaser = new AmazonPurchaser( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = AmazonPurchaser.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoPurchaser)); + + Field apiKeyField = AmazonPurchaser.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoPurchaser)); + } catch (NoSuchFieldException e) { + Assertions.fail(); + } catch (IllegalAccessException e) { + Assertions.fail(); + } + } + + @Test + void testCreateApiUrl() throws MalformedURLException { + // Test case 1 + String productId1 = "12345"; + URL expectedUrl1 = new URL( + "https://www.example.com/api/products/2020-08-26/products/" + + productId1 + + "/buy?productRegion=DE&locale=de_DE"); + URL createdUrl1 = demoPurchaser.createApiUrl(productId1); + Assertions.assertEquals(expectedUrl1, createdUrl1); + + // Test case 2 + String productId2 = "67890"; + URL expectedUrl2 = new URL( + "https://www.example.com/api/products/2020-08-26/" + + "products/" + + productId2 + + "/buy?productRegion=DE&locale=de_DE"); + URL createdUrl2 = demoPurchaser.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void getDataTarget_ReturnsExpectedDataOrigin() { + Webshop dataTarget = demoPurchaser.getDataTarget(); + assertEquals(demoDataTarget, dataTarget); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoPurchaser.getApiKey(); + assertEquals(demoApiKey, apiKey); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java new file mode 100644 index 0000000..d63d89c --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonSellerTest.java @@ -0,0 +1,82 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.model.Webshop; + +class AmazonSellerTest { + private AmazonSeller demoSeller; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static Webshop demoDataTarget = Webshop.AMAZON; + + @BeforeEach + void setup() { + demoSeller = new AmazonSeller( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = AmazonSeller.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoSeller)); + + Field apiKeyField = AmazonSeller.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoSeller)); + } catch (NoSuchFieldException e) { + Assertions.fail(); + } catch (IllegalAccessException e) { + Assertions.fail(); + } + } + + @Test + void testCreateApiUrl() throws MalformedURLException { + // Test case 1 + String productId1 = "12345"; + URL expectedUrl1 = new URL( + "https://www.example.com/api/products/2020-08-26/products/" + + productId1 + + "/sell?productRegion=DE&locale=de_DE"); + URL createdUrl1 = demoSeller.createApiUrl(productId1); + Assertions.assertEquals(expectedUrl1, createdUrl1); + + // Test case 2 + String productId2 = "67890"; + URL expectedUrl2 = new URL( + "https://www.example.com/api/products/2020-08-26/" + + "products/" + + productId2 + + "/sell?productRegion=DE&locale=de_DE"); + URL createdUrl2 = demoSeller.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void getDataTarget_ReturnsExpectedDataOrigin() { + Webshop dataTarget = demoSeller.getDataTarget(); + assertEquals(demoDataTarget, dataTarget); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoSeller.getApiKey(); + assertEquals(demoApiKey, apiKey); + } +} 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 f1c85b0..c152e5f 100644 --- a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java @@ -1,8 +1,6 @@ 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; @@ -11,17 +9,14 @@ 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 { @Mock private Config config; - private DataSourceFactory dataSourceFactory; + private DataSourceFactory DataSourceFactory; @BeforeEach void setUp() throws ConfigurationException { @@ -30,40 +25,24 @@ class DataSourceFactoryTest { when(config.getProperty("AMAZON_API_KEY")).thenReturn("amazon-api-key"); when(config.getProperty("EBAY_API_URL")).thenReturn("https://api.ebay.com"); when(config.getProperty("EBAY_API_KEY")).thenReturn("ebay-api-key"); - dataSourceFactory = new DataSourceFactory(config); + DataSourceFactory = new DataSourceFactory(config); } @Test void createAmazonProductDataSource_ReturnsAmazonProductDataSource() { // Act - AmazonProductDataSource dataSource = dataSourceFactory.createAmazonProductDataSource(); + AmazonProductDataSource DataSource = DataSourceFactory.createAmazonProductDataSource(); // Assert - assertEquals("amazon-api-key", dataSource.getApiKey()); + assertEquals("amazon-api-key", DataSource.getApiKey()); } @Test void createEbayItemDataSource_ReturnsEbayItemDataSource() { // Act - EbayItemDataSource dataSource = dataSourceFactory.createEbayItemDataSource(); + EbayItemDataSource DataSource = DataSourceFactory.createEbayItemDataSource(); // 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()); + assertEquals("ebay-api-key", DataSource.getApiKey()); } } diff --git a/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java index 9a6ed45..1615b44 100644 --- a/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java @@ -19,13 +19,14 @@ import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.exception.DataSourceException; +import de.rwu.easydrop.model.Webshop; class EbayItemDataSourceTest { private EbayItemDataSource demoDataSource; private static String demoApiKey = "my-api-key"; private static String demoApiUrl = "https://www.example.com/api"; - private static String demoDataOrigin = "eBay"; + private static Webshop demoDataOrigin = Webshop.EBAY; private static String demoQuery = "iPhone"; @BeforeEach @@ -82,7 +83,7 @@ class EbayItemDataSourceTest { ProductDTO result = demoDataSource.buildProductDTO(product, json); - assertEquals("eBay", result.getDataOrigin()); + assertEquals(Webshop.EBAY, result.getDataOrigin()); assertEquals(false, result.isAvailable()); // Default value for boolean assertEquals(0.0, result.getCurrentPrice()); // Default value for double assertEquals(0.0, result.getDeliveryPrice()); // Default value for double @@ -91,7 +92,7 @@ class EbayItemDataSourceTest { @Test void getDataOrigin_ReturnsExpectedDataOrigin() { - String dataOrigin = demoDataSource.getDataOrigin(); + Webshop dataOrigin = demoDataSource.getDataOrigin(); assertEquals(demoDataOrigin, dataOrigin); } @@ -105,21 +106,21 @@ class EbayItemDataSourceTest { void testGetProductDTOById_failedRequest() throws IOException { // Set up the test environment - EbayItemDataSource dataSource = mock(EbayItemDataSource.class); + EbayItemDataSource DataSource = mock(EbayItemDataSource.class); URL mockURL = mock(URL.class); - when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin); - when(dataSource.createApiUrl(demoQuery)).thenReturn(mockURL); - when(dataSource.getProductDTOById(demoQuery)).thenCallRealMethod(); + when(DataSource.getDataOrigin()).thenReturn(demoDataOrigin); + when(DataSource.createApiUrl(demoQuery)).thenReturn(mockURL); + when(DataSource.getProductDTOById(demoQuery)).thenCallRealMethod(); HttpURLConnection mockConnection = mock(HttpURLConnection.class); when(mockURL.openConnection()).thenReturn(mockConnection); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); // Invoke the method and verify the exception DataSourceException exception = assertThrows(DataSourceException.class, () -> { - dataSource.getProductDTOById(demoQuery); + DataSource.getProductDTOById(demoQuery); }); // Verify the exception message - assertEquals("Nothing found: eBay API responded with error code 404", exception.getMessage()); + assertEquals("Nothing found: EBAY API responded with error code 404", exception.getMessage()); } } diff --git a/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java b/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java new file mode 100644 index 0000000..d303e0b --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/EbayPurchaserTest.java @@ -0,0 +1,79 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.model.Webshop; + +class EbayPurchaserTest { + private EbayPurchaser demoPurchaser; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static Webshop demoDataTarget = Webshop.EBAY; + + @BeforeEach + void setup() { + demoPurchaser = new EbayPurchaser( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = EbayPurchaser.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoPurchaser)); + + Field apiKeyField = EbayPurchaser.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoPurchaser)); + } catch (NoSuchFieldException e) { + Assertions.fail(); + } catch (IllegalAccessException e) { + Assertions.fail(); + } + } + + @Test + void testCreateApiUrl() throws MalformedURLException { + // Test case 1 + String productId1 = "12345"; + URL expectedUrl1 = new URL( + "https://www.example.com/api/buy/" + + productId1); + URL createdUrl1 = demoPurchaser.createApiUrl(productId1); + Assertions.assertEquals(expectedUrl1, createdUrl1); + + // Test case 2 + String productId2 = "67890"; + URL expectedUrl2 = new URL( + "https://www.example.com/api/buy/" + + productId2); + URL createdUrl2 = demoPurchaser.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void getDataTarget_ReturnsExpectedDataOrigin() { + Webshop dataTarget = demoPurchaser.getDataTarget(); + assertEquals(demoDataTarget, dataTarget); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoPurchaser.getApiKey(); + assertEquals(demoApiKey, apiKey); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java b/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java new file mode 100644 index 0000000..e9f38ac --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/EbaySellerTest.java @@ -0,0 +1,79 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.reflect.Field; +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.model.Webshop; + +class EbaySellerTest { + private EbaySeller demoSeller; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static Webshop demoDataTarget = Webshop.EBAY; + + @BeforeEach + void setup() { + demoSeller = new EbaySeller( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = EbaySeller.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoSeller)); + + Field apiKeyField = EbaySeller.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoSeller)); + } catch (NoSuchFieldException e) { + Assertions.fail(); + } catch (IllegalAccessException e) { + Assertions.fail(); + } + } + + @Test + void testCreateApiUrl() throws MalformedURLException { + // Test case 1 + String productId1 = "12345"; + URL expectedUrl1 = new URL( + "https://www.example.com/api/sell/" + + productId1); + URL createdUrl1 = demoSeller.createApiUrl(productId1); + Assertions.assertEquals(expectedUrl1, createdUrl1); + + // Test case 2 + String productId2 = "67890"; + URL expectedUrl2 = new URL( + "https://www.example.com/api/sell/" + + productId2); + URL createdUrl2 = demoSeller.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void getDataTarget_ReturnsExpectedDataOrigin() { + Webshop dataTarget = demoSeller.getDataTarget(); + assertEquals(demoDataTarget, dataTarget); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoSeller.getApiKey(); + assertEquals(demoApiKey, apiKey); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/PurchaserFactoryTest.java b/src/test/java/de/rwu/easydrop/api/client/PurchaserFactoryTest.java new file mode 100644 index 0000000..c14285f --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/PurchaserFactoryTest.java @@ -0,0 +1,63 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.util.Config; + +class PurchaserFactoryTest { + + @Mock + private Config mockConfig; + + private PurchaserFactory purchaserFactory; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + purchaserFactory = new PurchaserFactory(mockConfig); + purchaserFactory.setConfig(mockConfig); + } + + @Test + void createAmazonPurchaser_ValidConfig_ReturnsAmazonPurchaser() { + // Arrange + String amazonApiUrl = "amazon_api_url"; + String amazonApiKey = "amazon_api_key"; + + when(mockConfig.getProperty("AMAZON_API_URL")).thenReturn(amazonApiUrl); + when(mockConfig.getProperty("AMAZON_API_KEY")).thenReturn(amazonApiKey); + + // Act + AmazonPurchaser amazonPurchaser = purchaserFactory.createAmazonPurchaser(); + + // Assert + assertNotNull(amazonPurchaser); + assertEquals(amazonApiKey, amazonPurchaser.getApiKey()); + } + + @Test + void createEbayPurchaser_ValidConfig_ReturnsEbayPurchaser() { + // Arrange + String ebayApiUrl = "ebay_api_url"; + String ebayApiKey = "ebay_api_key"; + + when(mockConfig.getProperty("EBAY_API_URL")).thenReturn(ebayApiUrl); + when(mockConfig.getProperty("EBAY_API_KEY")).thenReturn(ebayApiKey); + + // Act + EbayPurchaser ebayPurchaser = purchaserFactory.createEbayPurchaser(); + + // Assert + assertNotNull(ebayPurchaser); + assertEquals(ebayApiKey, ebayPurchaser.getApiKey()); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/client/SellerFactoryTest.java b/src/test/java/de/rwu/easydrop/api/client/SellerFactoryTest.java new file mode 100644 index 0000000..c3c27ee --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/SellerFactoryTest.java @@ -0,0 +1,63 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.util.Config; + +class SellerFactoryTest { + + @Mock + private Config mockConfig; + + private SellerFactory sellerFactory; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + sellerFactory = new SellerFactory(mockConfig); + sellerFactory.setConfig(mockConfig); + } + + @Test + void createAmazonSeller_ValidConfig_ReturnsAmazonSeller() { + // Arrange + String amazonApiUrl = "amazon_api_url"; + String amazonApiKey = "amazon_api_key"; + + when(mockConfig.getProperty("AMAZON_API_URL")).thenReturn(amazonApiUrl); + when(mockConfig.getProperty("AMAZON_API_KEY")).thenReturn(amazonApiKey); + + // Act + AmazonSeller amazonSeller = sellerFactory.createAmazonSeller(); + + // Assert + assertNotNull(amazonSeller); + assertEquals(amazonApiKey, amazonSeller.getApiKey()); + } + + @Test + void createEbaySeller_ValidConfig_ReturnsEbaySeller() { + // Arrange + String ebayApiUrl = "ebay_api_url"; + String ebayApiKey = "ebay_api_key"; + + when(mockConfig.getProperty("EBAY_API_URL")).thenReturn(ebayApiUrl); + when(mockConfig.getProperty("EBAY_API_KEY")).thenReturn(ebayApiKey); + + // Act + EbaySeller ebaySeller = sellerFactory.createEbaySeller(); + + // Assert + assertNotNull(ebaySeller); + assertEquals(ebayApiKey, ebaySeller.getApiKey()); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java index 1e16d7c..3c19d29 100644 --- a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java +++ b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java @@ -6,13 +6,15 @@ import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; +import de.rwu.easydrop.model.Webshop; + class ProductDTOTest { @Test void constructor_SetsProductIdAndDataOrigin() { // Arrange String productId = "12345"; - String dataOrigin = "Amazon"; + Webshop dataOrigin = Webshop.AMAZON; // Act ProductDTO productDTO = new ProductDTO(productId, dataOrigin); @@ -25,19 +27,19 @@ class ProductDTOTest { @Test void gettersAndSetters_WorkAsExpected() { // Arrange - ProductDTO productDTO = new ProductDTO("12345", "Amazon"); + ProductDTO productDTO = new ProductDTO("12345", Webshop.AMAZON); // Act and Assert assertEquals("12345", productDTO.getProductId()); - assertEquals("Amazon", productDTO.getDataOrigin()); + assertEquals(Webshop.AMAZON, productDTO.getDataOrigin()); // Modify fields productDTO.setProductId("54321"); - productDTO.setDataOrigin("eBay"); + productDTO.setDataOrigin(Webshop.EBAY); // Assert assertEquals("54321", productDTO.getProductId()); - assertEquals("eBay", productDTO.getDataOrigin()); + assertEquals(Webshop.EBAY, productDTO.getDataOrigin()); } @Test diff --git a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java index 2e54299..68ee485 100644 --- a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java +++ b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java @@ -8,45 +8,51 @@ 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 static org.mockito.Mockito.when; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; 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.OfferDTO; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.api.dto.TransactionDTO; import de.rwu.easydrop.exception.PersistenceException; +import de.rwu.easydrop.model.Webshop; -@TestInstance(Lifecycle.PER_CLASS) class SQLiteConnectorTest { private static final String TEST_PRODUCT_ID = "12345"; - private SQLiteConnector sqliteConnector; + private SQLiteConnector sqliteConnector = new SQLiteConnector(new SQLiteDataSource()); + @Mock + private Connection connection; + + @Mock + private PreparedStatement statement; + + @Mock + private ResultSet resultSet; @Mock private SQLiteDataSource mockDataSource; - @BeforeAll - public void setup() { - sqliteConnector = new SQLiteConnector(new SQLiteDataSource()); - } - @BeforeEach - public void clearDatabase() { + public void prepare() { MockitoAnnotations.openMocks(this); } @Test - void saveProduct_ValidProduct_SuccessfullySaved() { + void writeProduct_ValidProduct_SuccessfullySaved() { // Arrange sqliteConnector.clearData(); - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); - ProductDTO.setDataOrigin("Amazon"); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON); + ProductDTO.setDataOrigin(Webshop.AMAZON); ProductDTO.setProductId(TEST_PRODUCT_ID); ProductDTO.setCurrentPrice(9.99); ProductDTO.setMerchant("Sample Merchant"); @@ -54,12 +60,12 @@ class SQLiteConnectorTest { ProductDTO.setAvailable(true); // Act - assertDoesNotThrow(() -> sqliteConnector.saveProduct(ProductDTO)); + assertDoesNotThrow(() -> sqliteConnector.writeProduct(ProductDTO)); // Assert ProductDTO savedProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID); assertNotNull(savedProductDTO); - assertEquals("Amazon", savedProductDTO.getDataOrigin()); + assertEquals(Webshop.AMAZON, savedProductDTO.getDataOrigin()); assertEquals(TEST_PRODUCT_ID, savedProductDTO.getProductId()); assertEquals(9.99, savedProductDTO.getCurrentPrice()); assertEquals("Sample Merchant", savedProductDTO.getMerchant()); @@ -78,7 +84,7 @@ class SQLiteConnectorTest { // Assert assertNotNull(ProductDTO); - assertEquals("Amazon", ProductDTO.getDataOrigin()); + assertEquals(Webshop.AMAZON, ProductDTO.getDataOrigin()); assertEquals(TEST_PRODUCT_ID, ProductDTO.getProductId()); assertEquals(9.99, ProductDTO.getCurrentPrice()); assertEquals("Sample Merchant", ProductDTO.getMerchant()); @@ -111,14 +117,14 @@ class SQLiteConnectorTest { } @Test - void saveProduct_ThrowsPersistenceException_OnSQLException() throws SQLException { + void writeProduct_ThrowsPersistenceException_OnSQLException() throws SQLException { // Arrange - ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON); sqliteConnector.setDb(mockDataSource); doThrow(SQLException.class).when(mockDataSource).getConnection(); // Act and Assert - assertThrows(PersistenceException.class, () -> sqliteConnector.saveProduct(ProductDTO)); + assertThrows(PersistenceException.class, () -> sqliteConnector.writeProduct(ProductDTO)); } @Test @@ -143,53 +149,179 @@ class SQLiteConnectorTest { } 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); + ProductDTO productDTO = new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON); + productDTO.setCurrentPrice(9.99); + productDTO.setMerchant("Sample Merchant"); + productDTO.setDeliveryPrice(2.50); + productDTO.setAvailable(true); + sqliteConnector.writeProduct(productDTO); + } + + private void insertSampleOffer() { + ProductDTO p1 = new ProductDTO("p1", Webshop.AMAZON); + p1.setCurrentPrice(123f); + + ProductDTO p2 = new ProductDTO("p2", Webshop.EBAY); + p2.setCurrentPrice(234f); + + OfferDTO dto = new OfferDTO(); + dto.setOfferId("123"); + dto.setLastUpdate("2020-01-01 00:00:00"); + dto.setSourceProduct(p1); + dto.setTargetProduct(p2); + sqliteConnector.writeOffer(dto); + } + + private void insertSampleTransaction() { + TransactionDTO dto = new TransactionDTO(); + dto.setOfferId("123"); + dto.setEarnings(12f); + dto.setVolume(123f); + dto.setTransactionTime("2020-01-01 00:00:00"); + sqliteConnector.writeTX(dto); } @Test - void getDataOrigin_ReturnsCorrectDataOrigin() { + void outputTransactionsToLog_NothingThrown() { + insertSampleTransaction(); + + assertDoesNotThrow(() -> { + sqliteConnector.outputTransactionsToLog(); + }); + } + + @Test + void outputSummaryToLog_NothingThrown() { + insertSampleTransaction(); + + assertDoesNotThrow(() -> { + sqliteConnector.outputSummaryToLog(); + }); + } + + @Test + void outputTransactionsToLog_ThrowsPersistenceException_OnSQLException() throws SQLException { // Arrange - SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.outputTransactionsToLog()); + assertEquals("Something went wrong while reading transaction from SQLite", e.getMessage()); + } + + @Test + void outputSummaryToLog_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.outputSummaryToLog()); + assertEquals("Something went wrong while reading transaction summary from SQLite", e.getMessage()); + } + + @Test + void deleteOfferById_SuccessfulDelete() { + // Arrange + sqliteConnector.clearData(); + insertSampleOffer(); + + // Make sure it's there + assertEquals(1, sqliteConnector.getOffers().size()); // Act - String dataOrigin = connector.getDataOrigin(); + sqliteConnector.deleteOfferById("123"); // Assert - assertEquals("SQLite", dataOrigin); + assertEquals(0, sqliteConnector.getOffers().size()); } @Test - void getApiKey_UnsupportedOperationExceptionThrown() { - // Arrange - SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + void deleteOfferById_ThrowsPersistenceException_OnSQLException() throws SQLException { + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); // Act and Assert - assertThrows(UnsupportedOperationException.class, connector::getApiKey); + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.deleteOfferById("123")); + assertEquals("Something went wrong while deleting offer from SQLite", e.getMessage()); } @Test - void buildProductDTO_UnsupportedOperationExceptionThrown() { - // Arrange - SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); - ProductDTO product = new ProductDTO("ASIN123", "Amazon"); - String json = "{\"productId\":\"ASIN123\",\"dataOrigin\":\"Amazon\"}"; + void getOffers_ReturnsCorrectValues() { + sqliteConnector.clearData(); + assertEquals(0, sqliteConnector.getOffers().size()); - // Act and Assert - assertThrows(UnsupportedOperationException.class, () -> connector.buildProductDTO(product, json)); + insertSampleOffer(); + + assertEquals(1, sqliteConnector.getOffers().size()); } @Test - void createApiUrl_UnsupportedOperationExceptionThrown() { - // Arrange - SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); - String productIdentifier = "ASIN123"; + void getOffers_ThrowsPersistenceException_OnSQLException() throws SQLException { + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); // Act and Assert - assertThrows(UnsupportedOperationException.class, () -> connector.createApiUrl(productIdentifier)); + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.getOffers()); + assertEquals("Something went wrong while reading offers from SQLite", e.getMessage()); + } + + @Test + void writeOffer_ValidDTO_DataSavedToDatabase() throws SQLException { + // Arrange + OfferDTO offerDTO = new OfferDTO(); + offerDTO.setOfferId("123"); + offerDTO.setSourceProduct(new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON)); + offerDTO.setTargetProduct(new ProductDTO(TEST_PRODUCT_ID, Webshop.AMAZON)); + + String expectedQuery = "INSERT INTO offers (offerId, sourceWebshop, sourceId, sourcePrice, " + + "targetWebshop, targetId, targetPrice, lastupdate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + + when(connection.prepareStatement(expectedQuery)).thenReturn(statement); + + // Act + assertDoesNotThrow(() -> sqliteConnector.writeOffer(offerDTO)); + } + + @Test + void writeTX_NothingThrown() { + TransactionDTO dto = new TransactionDTO(); + + assertDoesNotThrow(() -> { + sqliteConnector.writeTX(dto); + }); + } + + @Test + void writeTX_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + TransactionDTO dto = new TransactionDTO(); + + // Act and Assert + PersistenceException e = assertThrows(PersistenceException.class, + () -> sqliteConnector.writeTX(dto)); + assertEquals("Something went wrong while saving transaction to SQLite", e.getMessage()); + } + + @Test + void getOfferDTOById_OfferExists_ReturnsProductDTO() { + // Arrange + sqliteConnector.clearData(); + insertSampleOffer(); + + // Act + OfferDTO offerDTO = sqliteConnector.getOfferDTOById("123"); + + // Assert + assertNotNull(offerDTO); + assertEquals("123", offerDTO.getOfferId()); + assertEquals("2020-01-01 00:00:00", offerDTO.getLastUpdate()); } } diff --git a/src/test/java/de/rwu/easydrop/exception/DataWriterExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/DataWriterExceptionTest.java new file mode 100644 index 0000000..fd00b3e --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/DataWriterExceptionTest.java @@ -0,0 +1,61 @@ +package de.rwu.easydrop.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class DataWriterExceptionTest { + + @Test + void constructor_WithMessage_SetsMessage() { + // Arrange + String message = "Data source error"; + + // Act + DataWriterException exception = new DataWriterException(message); + + // Assert + assertEquals(message, exception.getMessage()); + } + + @Test + void constructor_WithMessageAndCause_SetsMessageAndCause() { + // Arrange + String message = "Data source error"; + Throwable cause = new IllegalArgumentException("Invalid argument"); + + // Act + DataWriterException exception = new DataWriterException(message, cause); + + // Assert + assertEquals(message, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } + + @Test + void constructor_WithNullMessage_SetsNullMessage() { + // Act + DataWriterException exception = new DataWriterException(null); + + // Assert + assertEquals(null, exception.getMessage()); + } + + @Test + void constructor_WithNullCause_SetsNullCause() { + // Act + DataWriterException exception = new DataWriterException("Data source error", null); + + // Assert + assertEquals(null, exception.getCause()); + } + + @Test + void throw_DataWriterException() { + // Act and Assert + assertThrows(DataWriterException.class, () -> { + throw new DataWriterException("Data source error"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java new file mode 100644 index 0000000..684bc57 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/InvalidCatalogueExceptionTest.java @@ -0,0 +1,33 @@ +package de.rwu.easydrop.exception; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class InvalidCatalogueExceptionTest { + + @Test + void testConstructorWithMessage_ExceptionWithMessageCreated() { + // Arrange + String message = "Invalid catalogue"; + + // Act + InvalidCatalogueException exception = new InvalidCatalogueException(message); + + // Assert + Assertions.assertEquals(message, exception.getMessage()); + } + + @Test + void testConstructorWithMessageAndCause_ExceptionWithMessageAndCauseCreated() { + // Arrange + String message = "Invalid catalogue"; + Throwable cause = new RuntimeException("Cause exception"); + + // Act + InvalidCatalogueException exception = new InvalidCatalogueException(message, cause); + + // Assert + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertEquals(cause, exception.getCause()); + } +} diff --git a/src/test/java/de/rwu/easydrop/exception/InvalidOfferExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/InvalidOfferExceptionTest.java new file mode 100644 index 0000000..1dfd268 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/InvalidOfferExceptionTest.java @@ -0,0 +1,61 @@ +package de.rwu.easydrop.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class InvalidOfferExceptionTest { + + @Test + void constructor_WithMessage_SetsMessage() { + // Arrange + String message = "Invalid Offer data"; + + // Act + InvalidOfferException exception = new InvalidOfferException(message); + + // Assert + assertEquals(message, exception.getMessage()); + } + + @Test + void constructor_WithMessageAndCause_SetsMessageAndCause() { + // Arrange + String message = "Invalid Offer data"; + Throwable cause = new IllegalArgumentException("Invalid argument"); + + // Act + InvalidOfferException exception = new InvalidOfferException(message, cause); + + // Assert + assertEquals(message, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } + + @Test + void constructor_WithNullMessage_SetsNullMessage() { + // Act + InvalidOfferException exception = new InvalidOfferException(null); + + // Assert + assertEquals(null, exception.getMessage()); + } + + @Test + void constructor_WithNullCause_SetsNullCause() { + // Act + InvalidOfferException exception = new InvalidOfferException("Invalid Offer data", null); + + // Assert + assertEquals(null, exception.getCause()); + } + + @Test + void throw_InvalidOfferException() { + // Act and Assert + assertThrows(InvalidOfferException.class, () -> { + throw new InvalidOfferException("Invalid Offer data"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/exception/InvalidTransactionExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/InvalidTransactionExceptionTest.java new file mode 100644 index 0000000..32e4971 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/InvalidTransactionExceptionTest.java @@ -0,0 +1,61 @@ +package de.rwu.easydrop.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class InvalidTransactionExceptionTest { + + @Test + void constructor_WithMessage_SetsMessage() { + // Arrange + String message = "Invalid Transaction data"; + + // Act + InvalidTransactionException exception = new InvalidTransactionException(message); + + // Assert + assertEquals(message, exception.getMessage()); + } + + @Test + void constructor_WithMessageAndCause_SetsMessageAndCause() { + // Arrange + String message = "Invalid Transaction data"; + Throwable cause = new IllegalArgumentException("Invalid argument"); + + // Act + InvalidTransactionException exception = new InvalidTransactionException(message, cause); + + // Assert + assertEquals(message, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } + + @Test + void constructor_WithNullMessage_SetsNullMessage() { + // Act + InvalidTransactionException exception = new InvalidTransactionException(null); + + // Assert + assertEquals(null, exception.getMessage()); + } + + @Test + void constructor_WithNullCause_SetsNullCause() { + // Act + InvalidTransactionException exception = new InvalidTransactionException("Invalid Transaction data", null); + + // Assert + assertEquals(null, exception.getCause()); + } + + @Test + void throw_InvalidTransactionException() { + // Act and Assert + assertThrows(InvalidTransactionException.class, () -> { + throw new InvalidTransactionException("Invalid Transaction data"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java index f070e2d..df78e3e 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java @@ -1,5 +1,7 @@ package de.rwu.easydrop.model; + + import java.util.List; import org.junit.jupiter.api.Assertions; @@ -19,7 +21,7 @@ class ProductCatalogueTest { Product product = new Product(); product.setProductId("12345"); product.setMerchant("AmazonSeller"); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.AMAZON); productCatalogue.addProduct(product); List products = productCatalogue.getProducts(); @@ -32,13 +34,13 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.AMAZON); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); product2.setMerchant("eBaySeller"); - product2.setDataOrigin("eBay"); + product2.setDataOrigin(Webshop.EBAY); productCatalogue.addProduct(product2); productCatalogue.removeProduct(product1); @@ -53,13 +55,13 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.AMAZON); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); - product2.setMerchant("eBay"); - product2.setDataOrigin("eBay"); + product2.setMerchant("ebayMerchant"); + product2.setDataOrigin(Webshop.EBAY); productCatalogue.addProduct(product2); productCatalogue.clearProducts(); @@ -73,20 +75,23 @@ class ProductCatalogueTest { Product product1 = new Product(); product1.setProductId("12345"); product1.setMerchant("AmazonSeller"); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.AMAZON); productCatalogue.addProduct(product1); Product product2 = new Product(); product2.setProductId("54321"); product2.setMerchant("eBaySeller"); - product2.setDataOrigin("eBay"); + product2.setDataOrigin(Webshop.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"; + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Catalogue Name: %s%n", "GPU")); + sb.append(String.format("Description: %s%n", "Graphics Processing Units")); + sb.append("Products:\n"); + sb.append(String.format("%s%n", product1.toString())); + sb.append(String.format("%s%n", product2.toString())); + + String expectedString = sb.toString(); Assertions.assertEquals(expectedString, productCatalogue.toString()); } diff --git a/src/test/java/de/rwu/easydrop/model/ProductPairTest.java b/src/test/java/de/rwu/easydrop/model/ProductPairTest.java new file mode 100644 index 0000000..b562918 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/model/ProductPairTest.java @@ -0,0 +1,25 @@ +package de.rwu.easydrop.model; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +class ProductPairTest { + + @Test + void constructor_TwoProducts_ProductsInitializedCorrectly() { + // Arrange + Product product1 = new Product(); + product1.setProductId("123"); + + Product product2 = new Product(); + product2.setProductId("234"); + + // Act + ProductPair pair = new ProductPair(product1, product2); + + // Assert + assertSame(product1, pair.getProduct1()); + assertSame(product2, pair.getProduct2()); + } +} diff --git a/src/test/java/de/rwu/easydrop/model/ProductTest.java b/src/test/java/de/rwu/easydrop/model/ProductTest.java index 1845b65..10c8278 100644 --- a/src/test/java/de/rwu/easydrop/model/ProductTest.java +++ b/src/test/java/de/rwu/easydrop/model/ProductTest.java @@ -10,13 +10,13 @@ class ProductTest { @Test void testToString1() { Product product1 = new Product(); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.AMAZON); product1.setProductId("12345"); product1.setMerchant("Merchant A"); product1.setCurrentPrice(19.99); product1.setAvailable(true); - String expectedString1 = "Product: [12345 from Merchant A (Amazon) at 19,99 € (available: yes)]"; + String expectedString1 = "Product: [12345 from Merchant A (AMAZON) at 19,99 Euro (available: yes)]"; String result1 = product1.toString(); assertEquals(expectedString1, result1); @@ -25,13 +25,13 @@ class ProductTest { @Test void testToString2() { Product product2 = new Product(); - product2.setDataOrigin("eBay"); + product2.setDataOrigin(Webshop.EBAY); product2.setProductId("67890"); product2.setMerchant("Merchant B"); product2.setCurrentPrice(9.99); product2.setAvailable(false); - String expectedString2 = "Product: [67890 from Merchant B (eBay) at 9,99 € (available: no)]"; + String expectedString2 = "Product: [67890 from Merchant B (EBAY) at 9,99 Euro (available: no)]"; String result2 = product2.toString(); assertEquals(expectedString2, result2); @@ -41,7 +41,7 @@ class ProductTest { void gettersAndSetters_WorkAsExpected() { // Arrange Product product = new Product(); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.AMAZON); product.setProductId("12345"); product.setCurrentPrice(9.99); product.setMerchant("Example Merchant"); @@ -49,7 +49,7 @@ class ProductTest { product.setAvailable(true); // Act and Assert - assertEquals("Amazon", product.getDataOrigin()); + assertEquals(Webshop.AMAZON, product.getDataOrigin()); assertEquals("12345", product.getProductId()); assertEquals(9.99, product.getCurrentPrice()); assertEquals("Example Merchant", product.getMerchant()); @@ -57,7 +57,7 @@ class ProductTest { assertTrue(product.isAvailable()); // Modify fields - product.setDataOrigin("eBay"); + product.setDataOrigin(Webshop.EBAY); product.setProductId("54321"); product.setCurrentPrice(19.99); product.setMerchant("New Merchant"); @@ -65,7 +65,7 @@ class ProductTest { product.setAvailable(false); // Assert - assertEquals("eBay", product.getDataOrigin()); + assertEquals(Webshop.EBAY, product.getDataOrigin()); assertEquals("54321", product.getProductId()); assertEquals(19.99, product.getCurrentPrice()); assertEquals("New Merchant", product.getMerchant()); diff --git a/src/test/java/de/rwu/easydrop/model/WebshopTest.java b/src/test/java/de/rwu/easydrop/model/WebshopTest.java new file mode 100644 index 0000000..6e40870 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/model/WebshopTest.java @@ -0,0 +1,23 @@ +package de.rwu.easydrop.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class WebshopTest { + + @Test + void testFromString_returnsIntendedConstant() { + Webshop testShop = Webshop.fromString("Amazon"); + + assertEquals(Webshop.AMAZON, testShop); + } + + @Test + void testFromString_invalidShop() { + assertThrows(IllegalArgumentException.class, () -> { + Webshop.fromString("thisdoesnotexist"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java new file mode 100644 index 0000000..178588d --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/mapping/OfferMapperTest.java @@ -0,0 +1,62 @@ +package de.rwu.easydrop.service.mapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; + +class OfferMapperTest { + + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = OfferMapper.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + + } + + + @Test + void mapOfferToDTO() { + // Arrange + Product product = new Product(); + product.setProductId("12345"); + product.setDataOrigin(Webshop.AMAZON); + product.setAvailable(true); + product.setCurrentPrice(9.99); + product.setDeliveryPrice(2.50); + product.setMerchant("Seller1"); + + Offer offer = new Offer(); + offer.setOfferId("68735"); + offer.setLastUpdate("2020-07-07"); + offer.setSourceProduct(product); + offer.setTargetProduct(product); + + // Act + OfferDTO dto = OfferMapper.mapOfferToDTO(offer); + + // Assert + assertEquals("68735", dto.getOfferId()); + assertEquals("2020-07-07", dto.getLastUpdate()); + + } + +} 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 6239dbf..70bae6e 100644 --- a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java +++ b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test; import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; class ProductMapperTest { @Test @@ -47,7 +48,7 @@ class ProductMapperTest { } private ProductDTO createProductDTO() { - ProductDTO dto = new ProductDTO("12345", "Amazon"); + ProductDTO dto = new ProductDTO("12345", Webshop.AMAZON); dto.setAvailable(true); dto.setCurrentPrice(9.99); dto.setDeliveryPrice(2.50); @@ -60,7 +61,7 @@ class ProductMapperTest { // Arrange Product product = new Product(); product.setProductId("12345"); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.AMAZON); product.setAvailable(true); product.setCurrentPrice(9.99); product.setDeliveryPrice(2.50); @@ -71,7 +72,7 @@ class ProductMapperTest { // Assert assertEquals("12345", dto.getProductId()); - assertEquals("Amazon", dto.getDataOrigin()); + assertEquals(Webshop.AMAZON, dto.getDataOrigin()); assertTrue(dto.isAvailable()); assertEquals(9.99, dto.getCurrentPrice()); assertEquals(2.50, dto.getDeliveryPrice()); diff --git a/src/test/java/de/rwu/easydrop/service/mapping/TransactionMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/TransactionMapperTest.java new file mode 100644 index 0000000..79de525 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/mapping/TransactionMapperTest.java @@ -0,0 +1,71 @@ +package de.rwu.easydrop.service.mapping; + +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.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.model.Transaction; + +class TransactionMapperTest { + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = TransactionMapper.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + @Test + void mapTXFromDTO_WithValidTransactionDTO_ReturnsTransaction() { + // Arrange + TransactionDTO transactionDTO = new TransactionDTO(); + transactionDTO.setOfferId("123"); + transactionDTO.setVolume(10.0); + transactionDTO.setEarnings(5.0); + transactionDTO.setTransactionTime("2023-06-25 03:26:59"); + + // Act + Transaction transaction = TransactionMapper.mapTXFromDTO(transactionDTO); + + // Assert + assertNotNull(transaction); + assertEquals(transactionDTO.getOfferId(), transaction.getOfferId()); + assertEquals(transactionDTO.getVolume(), transaction.getVolume()); + assertEquals(transactionDTO.getEarnings(), transaction.getEarnings()); + assertEquals(transactionDTO.getTransactionTime(), transaction.getTransactionTime()); + } + + @Test + void mapTXToDTO_WithValidTransaction_ReturnsTransactionDTO() { + // Arrange + Transaction transaction = new Transaction(); + transaction.setOfferId("123"); + transaction.setVolume(10.0); + transaction.setEarnings(5.0); + transaction.setTransactionTime("2023-06-25 03:26:59"); + + // Act + TransactionDTO transactionDTO = TransactionMapper.mapTXToDTO(transaction); + + // Assert + assertNotNull(transactionDTO); + assertEquals(transaction.getOfferId(), transactionDTO.getOfferId()); + assertEquals(transaction.getVolume(), transactionDTO.getVolume()); + assertEquals(transaction.getEarnings(), transactionDTO.getEarnings()); + assertEquals(transaction.getTransactionTime(), transactionDTO.getTransactionTime()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java b/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java new file mode 100644 index 0000000..466bd56 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/processing/OfferIdentifierTest.java @@ -0,0 +1,176 @@ +package de.rwu.easydrop.service.processing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +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.exception.InvalidCatalogueException; +import de.rwu.easydrop.exception.InvalidOfferException; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.ProductPair; + +class OfferIdentifierTest { + + @Mock + private ProductCatalogue mockProductCatalogue; + + private OfferIdentifier offerIdentifier; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + offerIdentifier = new OfferIdentifier(); + } + + @Test + void runIdentifier_WithValidProductCatalogues_IdentifiesOffers() { + // Arrange + List productCatalogues = new ArrayList<>(); + ProductCatalogue productCatalogue1 = new ProductCatalogue("Catalogue1", "desc"); + ProductCatalogue productCatalogue2 = new ProductCatalogue("Catalogue2", "desc"); + + Product product1 = new Product(); + product1.setCurrentPrice(100.0); + Product product2 = new Product(); + product2.setCurrentPrice(200.0); + Product product3 = new Product(); + product3.setCurrentPrice(300.0); + Product product4 = new Product(); + product4.setCurrentPrice(400.0); + + productCatalogue1.addProduct(product1); + productCatalogue1.addProduct(product2); + productCatalogue2.addProduct(product3); + productCatalogue2.addProduct(product4); + + productCatalogues.add(productCatalogue1); + productCatalogues.add(productCatalogue2); + + // Act + List identifiedOffers = offerIdentifier.runIdentifier(productCatalogues); + + // Assert + assertEquals(2, identifiedOffers.size()); + assertNotNull(identifiedOffers.get(0).getSourceProduct()); + assertNotNull(identifiedOffers.get(0).getTargetProduct()); + assertNotNull(identifiedOffers.get(1).getSourceProduct()); + assertNotNull(identifiedOffers.get(1).getTargetProduct()); + } + + @Test + void runIdentifier_WithOneGoodOneBadCatalogue_BadOneIsntIncluded() { + // Arrange + List productCatalogues = new ArrayList<>(); + ProductCatalogue productCatalogue1 = new ProductCatalogue("Catalogue1", "desc"); + ProductCatalogue productCatalogue2 = new ProductCatalogue("Catalogue2", "desc"); + + Product product1 = new Product(); + product1.setCurrentPrice(100.0); + Product product2 = new Product(); + product2.setCurrentPrice(200.0); + Product product3 = new Product(); + product3.setCurrentPrice(300.0); + Product product4 = new Product(); + product4.setCurrentPrice(300.0); + + productCatalogue1.addProduct(product1); + productCatalogue1.addProduct(product2); + productCatalogue2.addProduct(product3); + productCatalogue2.addProduct(product4); + + productCatalogues.add(productCatalogue1); + productCatalogues.add(productCatalogue2); + + // Act + List identifiedOffers = offerIdentifier.runIdentifier(productCatalogues); + + // Assert + assertEquals(1, identifiedOffers.size()); // Only 1 passed + } + + @Test + void getHighestMarginProducts_WithValidProductCatalogue_ReturnsProductPair() { + // Arrange + ProductCatalogue productCatalogue = new ProductCatalogue("TestCatalogue", "desc"); + Product product1 = new Product(); + product1.setCurrentPrice(100.0); + Product product2 = new Product(); + product2.setCurrentPrice(200.0); + productCatalogue.addProduct(product1); + productCatalogue.addProduct(product2); + + // Act + ProductPair productPair = OfferIdentifier.getHighestMarginProducts(productCatalogue); + + // Assert + assertNotNull(productPair); + assertNotNull(productPair.getProduct1()); + assertNotNull(productPair.getProduct2()); + assertNotEquals(productPair.getProduct1(), productPair.getProduct2()); + } + + @Test + void getHighestMarginProducts_SecondProductCheaper() { + // Arrange + ProductCatalogue productCatalogue = new ProductCatalogue("TestCatalogue", "desc"); + Product product1 = new Product(); + product1.setCurrentPrice(200.0); + Product product2 = new Product(); + product2.setCurrentPrice(100.0); + productCatalogue.addProduct(product1); + productCatalogue.addProduct(product2); + + // Act + ProductPair productPair = OfferIdentifier.getHighestMarginProducts(productCatalogue); + + // Assert + assertNotNull(productPair); + assertNotNull(productPair.getProduct1()); + assertNotNull(productPair.getProduct2()); + assertNotEquals(productPair.getProduct1(), productPair.getProduct2()); + } + + @Test + void getHighestMarginProducts_WithCatalogueSizeLessThan2_ThrowsInvalidCatalogueException() { + // Arrange + ProductCatalogue productCatalogue = new ProductCatalogue("SmallCatalogue", "desc"); + + // Act and Assert + assertThrows(InvalidCatalogueException.class, () -> { + OfferIdentifier.getHighestMarginProducts(productCatalogue); + }); + } + + @Test + void getHighestMarginProducts_WithZeroPriceMargin_ThrowsInvalidOfferException() { + // Arrange + ProductCatalogue productCatalogue = new ProductCatalogue("name", "desc"); + Product product1 = new Product(); + product1.setCurrentPrice(200.0); + + Product product2 = new Product(); + product2.setCurrentPrice(200.0); + + productCatalogue.getProducts().add(product1); + productCatalogue.getProducts().add(product2); + + // Act and Assert + InvalidOfferException e = assertThrows(InvalidOfferException.class, () -> { + OfferIdentifier.getHighestMarginProducts(productCatalogue); + }); + + assertEquals("Price margin is zero!", e.getMessage()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java b/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java new file mode 100644 index 0000000..3b3df76 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/processing/OfferProvisionerTest.java @@ -0,0 +1,105 @@ +package de.rwu.easydrop.service.processing; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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.client.AmazonPurchaser; +import de.rwu.easydrop.api.client.AmazonSeller; +import de.rwu.easydrop.api.client.EbayPurchaser; +import de.rwu.easydrop.api.client.EbaySeller; +import de.rwu.easydrop.api.client.PurchaserFactory; +import de.rwu.easydrop.api.client.SellerFactory; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; +import de.rwu.easydrop.service.writer.OfferWriter; + +class OfferProvisionerTest { + + @Mock + private OfferPersistenceInterface mockPersistence; + + @Mock + private SellerFactory mockSellerFactory; + + @Mock + private PurchaserFactory mockPurchaserFactory; + + @Mock + private OfferWriter mockOfferWriter; + + @Mock + private AmazonSeller mockAmazonSeller; + + @Mock + private EbaySeller mockEbaySeller; + + @Mock + private AmazonPurchaser mockAmazonPurchaser; + + @Mock + private EbayPurchaser mockEbayPurchaser; + + private OfferProvisioner offerProvisioner; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + when(mockSellerFactory.createAmazonSeller()).thenReturn(mockAmazonSeller); + when(mockSellerFactory.createEbaySeller()).thenReturn(mockEbaySeller); + when(mockPurchaserFactory.createAmazonPurchaser()).thenReturn(mockAmazonPurchaser); + when(mockPurchaserFactory.createEbayPurchaser()).thenReturn(mockEbayPurchaser); + + offerProvisioner = new OfferProvisioner(mockPersistence, mockSellerFactory, mockPurchaserFactory); + offerProvisioner.setOfferWriter(mockOfferWriter); + } + + @Test + void runProvisioner_WithValidOffers_PlacesOrders() { + // Arrange + List offers = new ArrayList<>(); + Offer offer1 = createOffer(Webshop.AMAZON, "ASIN123", Webshop.EBAY, "12345"); + Offer offer2 = createOffer(Webshop.EBAY, "54321", Webshop.AMAZON, "ASIN456"); + offers.add(offer1); + offers.add(offer2); + + // Act + offerProvisioner.runProvisioner(offers); + + // Assert + verify(mockAmazonSeller, times(1)).sellProduct(any()); + verify(mockEbaySeller, times(1)).sellProduct(any()); + verify(mockAmazonPurchaser, times(1)).purchaseProduct(any()); + verify(mockEbayPurchaser, times(1)).purchaseProduct(any()); + verify(mockOfferWriter, times(2)).writeOfferToPersistence(any()); + } + + private Offer createOffer(Webshop sourceWebshop, String sourceProductId, Webshop targetWebshop, + String targetProductId) { + Product sourceProduct = new Product(); + sourceProduct.setDataOrigin(sourceWebshop); + sourceProduct.setProductId(sourceProductId); + + Product targetProduct = new Product(); + targetProduct.setDataOrigin(targetWebshop); + targetProduct.setProductId(targetProductId); + + Offer offer = new Offer(); + offer.setSourceProduct(sourceProduct); + offer.setTargetProduct(targetProduct); + + return offer; + } +} diff --git a/src/test/java/de/rwu/easydrop/service/processing/TransactionHandlerTest.java b/src/test/java/de/rwu/easydrop/service/processing/TransactionHandlerTest.java new file mode 100644 index 0000000..4527551 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/processing/TransactionHandlerTest.java @@ -0,0 +1,50 @@ +package de.rwu.easydrop.service.processing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.TransactionDTO; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Transaction; +import de.rwu.easydrop.service.mapping.TransactionMapper; + +class TransactionHandlerTest { + + @Mock + private TransactionPersistenceInterface mockPersistence; + + public TransactionHandlerTest() { + MockitoAnnotations.openMocks(this); + } + + @Test + void turnOfferToTransaction_ValidOffer_TransactionCreatedAndWrittenToPersistence() { + // Arrange + Offer offer = new Offer(); + Product product = new Product(); + offer.setSourceProduct(product); + offer.setTargetProduct(product); + + TransactionHandler handler = new TransactionHandler(mockPersistence); + + // Act + handler.turnOfferToTransaction(offer); + + // Assert + ArgumentCaptor transactionDtoCaptor = ArgumentCaptor.forClass(TransactionDTO.class); + verify(mockPersistence, times(1)).writeTX(transactionDtoCaptor.capture()); + + TransactionDTO capturedTransactionDto = transactionDtoCaptor.getValue(); + Transaction capturedTransaction = TransactionMapper.mapTXFromDTO(capturedTransactionDto); + + assertEquals(offer.getOfferId(), capturedTransaction.getOfferId()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java index acaca80..b13c571 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java @@ -1,7 +1,6 @@ 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; @@ -14,9 +13,9 @@ 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.model.Webshop; import de.rwu.easydrop.util.ProductsConfig; class CatalogueRetrieverTest { @@ -39,20 +38,20 @@ class CatalogueRetrieverTest { // Create a sample product catalogue with two products ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue"); Product product1 = new Product(); - product1.setDataOrigin("Amazon"); + product1.setDataOrigin(Webshop.AMAZON); product1.setProductId("ASIN1"); productCatalogue.addProduct(product1); Product product2 = new Product(); - product2.setDataOrigin("eBay"); + product2.setDataOrigin(Webshop.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); + when(productRetriever.getProductFromWebshop(Webshop.AMAZON, "ASIN1")).thenReturn(product1); + when(productRetriever.getProductFromWebshop(Webshop.EBAY, "ProductID2")).thenReturn(product2); // Act catalogueRetriever.loadCatalogues(); @@ -71,33 +70,7 @@ class CatalogueRetrieverTest { // 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()); + verify(productRetriever).getProductFromWebshop(Webshop.AMAZON, "ASIN1"); + verify(productRetriever).getProductFromWebshop(Webshop.EBAY, "ProductID2"); } } diff --git a/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java new file mode 100644 index 0000000..aa95f42 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/retriever/OfferRetrieverTest.java @@ -0,0 +1,82 @@ +package de.rwu.easydrop.service.retriever; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.naming.ConfigurationException; + +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.client.AmazonProductDataSource; +import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.api.client.EbayItemDataSource; +import de.rwu.easydrop.api.dto.OfferDTO; +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.util.Config; + + +class OfferRetrieverTest { + + @Mock + private Config config; + @Mock + private DataSourceFactory dataSourceFactory; + @Mock + private AmazonProductDataSource amazonDataSource; + @Mock + private EbayItemDataSource ebayDataSource; + @Mock + private OfferDTO offerDTO; + @Mock + private Offer offer; + @Mock + private OfferPersistenceInterface persistence; + + private OfferRetriever offerRetriever; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + when(config.getProperty("AMAZON_API_URL")).thenReturn("https://api.amazon.com"); + when(config.getProperty("AMAZON_API_KEY")).thenReturn("amazon-api-key"); + dataSourceFactory.setConfig(config); + offerRetriever = new OfferRetriever(persistence); + } + + @Test + void getOfferFromPersistence(){ + + //Mock ProductDTO object to return from the offerDTO mock + ProductDTO productDTO = mock(ProductDTO.class); + when(productDTO.isAvailable()).thenReturn(true); + + //Arrange + String offerId = "187"; + String lastUpdate = "2023-01-01"; + when(persistence.getOfferDTOById(offerId)).thenReturn(offerDTO); + when(offerDTO.getOfferId()).thenReturn(offerId); + when(offerDTO.getLastUpdate()).thenReturn(lastUpdate); + when(offerDTO.getSourceProduct()).thenReturn(productDTO); + when(offerDTO.getTargetProduct()).thenReturn(productDTO); + + //Act + Offer result = offerRetriever.getOfferFromPersistence(offerId); + + //Assert + assertEquals(offerId, result.getOfferId()); + assertEquals(lastUpdate, result.getLastUpdate()); + + //Verify + verify(persistence, times(1)).getOfferDTOById(offerId); + + } + +} 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 32afecc..5d0a2db 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java @@ -2,6 +2,8 @@ package de.rwu.easydrop.service.retriever; 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.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -17,8 +19,9 @@ 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.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; import de.rwu.easydrop.util.Config; class ProductRetrieverTest { @@ -35,7 +38,7 @@ class ProductRetrieverTest { @Mock private Product product; @Mock - private AbstractProductPersistence persistence; + private ProductPersistenceInterface persistence; private ProductRetriever productRetriever; @@ -45,7 +48,7 @@ class ProductRetrieverTest { when(config.getProperty("AMAZON_API_URL")).thenReturn("https://api.amazon.com"); when(config.getProperty("AMAZON_API_KEY")).thenReturn("amazon-api-key"); dataSourceFactory.setConfig(config); - productRetriever = new ProductRetriever(dataSourceFactory); + productRetriever = new ProductRetriever(dataSourceFactory, persistence); } @Test @@ -56,7 +59,7 @@ class ProductRetrieverTest { when(amazonDataSource.getProductDTOById(asin)).thenReturn(productDTO); when(productDTO.getProductId()).thenReturn(asin); when(productDTO.getCurrentPrice()).thenReturn(9.99); - when(productDTO.getDataOrigin()).thenReturn("Amazon"); + when(productDTO.getDataOrigin()).thenReturn(Webshop.AMAZON); // Act Product result = productRetriever.getProductFromAmazon(asin); @@ -76,7 +79,7 @@ class ProductRetrieverTest { when(ebayDataSource.getProductDTOById(productQuery)).thenReturn(productDTO); when(productDTO.getProductId()).thenReturn(productQuery); when(productDTO.getCurrentPrice()).thenReturn(9.99); - when(productDTO.getDataOrigin()).thenReturn("eBay"); + when(productDTO.getDataOrigin()).thenReturn(Webshop.EBAY); // Act Product result = productRetriever.getProductFromEbay(productQuery); @@ -92,11 +95,10 @@ class ProductRetrieverTest { 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"); + when(productDTO.getDataOrigin()).thenReturn(Webshop.AMAZON); // Act Product result = productRetriever.getProductFromPersistence(productId); @@ -108,4 +110,49 @@ class ProductRetrieverTest { // Verify the interactions verify(persistence, times(1)).getProductDTOById(productId); } + + @Test + void getProductFromWebshop_ValidAmazonWebshop_ReturnsProduct() { + // Arrange + String productIdentifier = "ASIN1"; + ProductDTO dto = new ProductDTO(productIdentifier, Webshop.AMAZON); + dto.setCurrentPrice(1.23); + AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + Product expectedProduct = new Product(); + expectedProduct.setProductId(productIdentifier); + expectedProduct.setDataOrigin(Webshop.AMAZON); + expectedProduct.setCurrentPrice(1.23); + when(dataSource.getProductDTOById(productIdentifier)).thenReturn(dto); + when(dataSourceFactory.createAmazonProductDataSource()).thenReturn(dataSource); + + // Act + Product actualProduct = productRetriever.getProductFromWebshop(Webshop.AMAZON, productIdentifier); + + // Assert + assertEquals(expectedProduct, actualProduct); + verify(dataSourceFactory).createAmazonProductDataSource(); + verify(dataSource).getProductDTOById(productIdentifier); + } + + @Test + void getProductFromWebshop_ValidEbayWebshop_ReturnsProduct() { + // Arrange + String productIdentifier = "ASIN1"; + ProductDTO dto = new ProductDTO(productIdentifier, Webshop.EBAY); + dto.setCurrentPrice(1.23); + Product expectedProduct = new Product(); + expectedProduct.setProductId(productIdentifier); + expectedProduct.setDataOrigin(Webshop.EBAY); + expectedProduct.setCurrentPrice(1.23); + when(ebayDataSource.getProductDTOById(productIdentifier)).thenReturn(dto); + when(dataSourceFactory.createEbayItemDataSource()).thenReturn(ebayDataSource); + + // Act + Product actualProduct = productRetriever.getProductFromWebshop(Webshop.EBAY, productIdentifier); + + // Assert + assertEquals(expectedProduct, actualProduct); + verify(dataSourceFactory).createEbayItemDataSource(); + verify(ebayDataSource).getProductDTOById(productIdentifier); + } } diff --git a/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java new file mode 100644 index 0000000..d9271de --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/validation/OfferValidatorTest.java @@ -0,0 +1,86 @@ +package de.rwu.easydrop.service.validation; + +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.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import de.rwu.easydrop.exception.InvalidOfferException; +import de.rwu.easydrop.model.Offer; + +class OfferValidatorTest { + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = OfferValidator.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + @Test + void validate_Offer_ValidProduct_NoExceptionThrown() { + // Arrange + Offer offer = new Offer(); + offer.setOfferId("3672"); + offer.setLastUpdate("2021-02-03"); + + // Act and Assert + assertDoesNotThrow(() -> OfferValidator.validate(offer)); + } + + @Test + void validate_Offer_NullPointer() { + // Arrange + Offer offer = new Offer(); + + InvalidOfferException e = assertThrows(InvalidOfferException.class, () -> { + OfferValidator.validate(offer); + }); + + assertEquals("Required information is missing in the offer", e.getMessage()); + } + + @ParameterizedTest + @MethodSource("invalidOfferProvider") + void validate_InvalidOffer_ThrowsInvalidOfferException(Offer offer) { + // Act and Assert + assertThrows(InvalidOfferException.class, () -> OfferValidator.validate(offer)); + + } + + static Stream invalidOfferProvider() { + return Stream.of( + createOfferWithEmptylastUpdate(), + createOfferWithEmptyId()); + } + + private static Offer createOfferWithEmptylastUpdate() { + Offer offer = new Offer(); + offer.setOfferId("3729798"); + offer.setLastUpdate(""); + return offer; + } + + private static Offer createOfferWithEmptyId() { + Offer offer = new Offer(); + offer.setOfferId(""); + offer.setLastUpdate("8798476"); + return offer; + } + +} diff --git a/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java index 7827bbb..4058ea7 100644 --- a/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java +++ b/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java @@ -1,7 +1,6 @@ package de.rwu.easydrop.service.validation; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -16,6 +15,7 @@ import org.junit.jupiter.params.provider.MethodSource; import de.rwu.easydrop.exception.InvalidProductException; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; class ProductValidatorTest { @@ -38,37 +38,13 @@ class ProductValidatorTest { // Arrange Product product = new Product(); product.setCurrentPrice(9.99); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.AMAZON); product.setProductId("12345"); // Act and Assert assertDoesNotThrow(() -> ProductValidator.validate(product)); } - @Test - void isInValidDataOrigins_ValidDataOrigin_ReturnsTrue() { - // Arrange - String dataOrigin = "Amazon"; - - // Act - boolean result = ProductValidator.isInValidDataOrigins(dataOrigin); - - // Assert - assertTrue(result); - } - - @Test - void isInValidDataOrigins_InvalidDataOrigin_ReturnsFalse() { - // Arrange - String dataOrigin = "UnknownOrigin"; - - // Act - boolean result = ProductValidator.isInValidDataOrigins(dataOrigin); - - // Assert - assertFalse(result); - } - @ParameterizedTest @MethodSource("invalidProductProvider") void validate_InvalidProduct_ThrowsInvalidProductException(Product product) { @@ -79,22 +55,13 @@ class ProductValidatorTest { static Stream invalidProductProvider() { return Stream.of( createProductWithZeroPrice(), - createProductWithUnknownDataOrigin(), createProductWithEmptyProductId()); } private static Product createProductWithZeroPrice() { Product product = new Product(); product.setCurrentPrice(0.00); - product.setDataOrigin("Amazon"); - product.setProductId("12345"); - return product; - } - - private static Product createProductWithUnknownDataOrigin() { - Product product = new Product(); - product.setCurrentPrice(9.99); - product.setDataOrigin("UnknownOrigin"); + product.setDataOrigin(Webshop.AMAZON); product.setProductId("12345"); return product; } @@ -102,7 +69,7 @@ class ProductValidatorTest { private static Product createProductWithEmptyProductId() { Product product = new Product(); product.setCurrentPrice(9.99); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.AMAZON); product.setProductId(""); return product; } diff --git a/src/test/java/de/rwu/easydrop/service/validation/TransactionValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/TransactionValidatorTest.java new file mode 100644 index 0000000..939379e --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/validation/TransactionValidatorTest.java @@ -0,0 +1,70 @@ +package de.rwu.easydrop.service.validation; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; + +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.exception.InvalidTransactionException; +import de.rwu.easydrop.model.Transaction; + +class TransactionValidatorTest { + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = TransactionValidator.class + .getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + @Test + void validate_ValidTransaction_NoExceptionThrown() { + // Arrange + Transaction transaction = new Transaction(); + transaction.setOfferId("123"); + transaction.setVolume(123f); + transaction.setEarnings(12.23); + transaction.setTransactionTime("2020-01-01 00:00:00"); + + // Act & Assert + assertDoesNotThrow(() -> TransactionValidator.validate(transaction)); + } + + @Test + void validate_InvalidTransaction_InvalidTransactionExceptionThrown() { + // Arrange + Transaction tx1 = new Transaction(); + tx1.setOfferId(""); // Set an empty Offer ID to make it invalid + Transaction tx2 = new Transaction(); + tx2.setOfferId("123"); + Transaction tx3 = new Transaction(); + tx3.setOfferId("123"); + tx3.setEarnings(123f); + + // Act & Assert + assertThrows(InvalidTransactionException.class, () -> TransactionValidator.validate(tx1)); + assertThrows(InvalidTransactionException.class, () -> TransactionValidator.validate(tx2)); + assertThrows(InvalidTransactionException.class, () -> TransactionValidator.validate(tx3)); + } + + @Test + void validate_NullTransaction_InvalidTransactionExceptionThrown() { + // Arrange + Transaction transaction = null; + + // Act & Assert + assertThrows(InvalidTransactionException.class, () -> TransactionValidator.validate(transaction)); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java index 71fdff5..22eb73b 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java @@ -13,14 +13,15 @@ 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.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.model.Webshop; class CatalogueWriterTest { @Mock - private AbstractProductPersistence persistenceMock; + private ProductPersistenceInterface persistenceMock; private CatalogueWriter catalogueWriter; @@ -39,19 +40,19 @@ class CatalogueWriterTest { catalogueWriter.writeCatalogues(catalogues); // Assert - verify(persistenceMock, times(4)).saveProduct(any(ProductDTO.class)); + verify(persistenceMock, times(4)).writeProduct(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")); + catalogue1.addProduct(createSampleProduct(Webshop.AMAZON, "ID 1")); + catalogue1.addProduct(createSampleProduct(Webshop.EBAY, "ID 2")); ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue 2", "Sample catalogue 2"); - catalogue2.addProduct(createSampleProduct("Amazon", "ID 3")); - catalogue2.addProduct(createSampleProduct("eBay", "ID 4")); + catalogue2.addProduct(createSampleProduct(Webshop.AMAZON, "ID 3")); + catalogue2.addProduct(createSampleProduct(Webshop.EBAY, "ID 4")); catalogues.add(catalogue1); catalogues.add(catalogue2); @@ -59,7 +60,7 @@ class CatalogueWriterTest { return catalogues; } - private Product createSampleProduct(String dataOrigin, String productId) { + private Product createSampleProduct(Webshop dataOrigin, String productId) { Product product = new Product(); product.setDataOrigin(dataOrigin); product.setProductId(productId); diff --git a/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java new file mode 100644 index 0000000..62ef077 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/writer/OfferWriterTest.java @@ -0,0 +1,78 @@ +package de.rwu.easydrop.service.writer; + +import static org.mockito.ArgumentMatchers.any; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +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.OfferDTO; +import de.rwu.easydrop.data.connector.OfferPersistenceInterface; +import de.rwu.easydrop.model.Offer; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; + +class OfferWriterTest { + + @Mock + private OfferDTO offerDTO; + @Mock + private OfferPersistenceInterface persistence; + + private OfferWriter offerWriter; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + offerWriter = new OfferWriter(persistence); + } + + @Test + void writeOfferToPersistence_InvalidProduct_ThrowsException() { + //Arrange + Offer offer = new Offer(); + offer.setOfferId(""); + offer.setLastUpdate(""); + + //Act and Assert + assertThrows(Exception.class, () -> offerWriter.writeOfferToPersistence(offer)); + } + + @Test + void writeOfferToPresistence_ValidProduct_CallsSaveProduct(){ + + // Arrange + Product product = new Product(); + product.setProductId("12345"); + product.setDataOrigin(Webshop.AMAZON); + product.setCurrentPrice(9.99); + + // Arrange + Offer offer = new Offer(); + offer.setOfferId("26876"); + offer.setLastUpdate("2022-12-25"); + offer.setSourceProduct(product); + offer.setTargetProduct(product); + + // Act + offerWriter.writeOfferToPersistence(offer); + + // Assert + Mockito.verify(persistence).writeOffer(any(OfferDTO.class)); + + } + + + + + + + + + + +} diff --git a/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java index aab0cdd..60f9198 100644 --- a/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java +++ b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java @@ -10,21 +10,21 @@ 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.data.connector.ProductPersistenceInterface; import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.Webshop; class ProductWriterTest { @Mock - private AbstractProductPersistence persistence; + private ProductPersistenceInterface persistence; private ProductWriter productWriter; @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); - productWriter = new ProductWriter(); - productWriter.setPersistence(persistence); + productWriter = new ProductWriter(persistence); } @Test @@ -32,14 +32,14 @@ class ProductWriterTest { // Arrange Product product = new Product(); product.setProductId("12345"); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.AMAZON); product.setCurrentPrice(9.99); // Act productWriter.writeProductToPersistence(product); // Assert - Mockito.verify(persistence).saveProduct(any(ProductDTO.class)); + Mockito.verify(persistence).writeProduct(any(ProductDTO.class)); } @Test @@ -47,7 +47,7 @@ class ProductWriterTest { // Arrange Product product = new Product(); product.setProductId(""); - product.setDataOrigin("Amazon"); + product.setDataOrigin(Webshop.AMAZON); // Act and Assert assertThrows(Exception.class, () -> productWriter.writeProductToPersistence(product)); diff --git a/src/test/java/de/rwu/easydrop/service/writer/TransactionWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/TransactionWriterTest.java new file mode 100644 index 0000000..ef9dd48 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/writer/TransactionWriterTest.java @@ -0,0 +1,63 @@ +package de.rwu.easydrop.service.writer; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +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.TransactionDTO; +import de.rwu.easydrop.data.connector.TransactionPersistenceInterface; +import de.rwu.easydrop.exception.InvalidTransactionException; +import de.rwu.easydrop.model.Transaction; +import de.rwu.easydrop.service.mapping.TransactionMapper; + +class TransactionWriterTest { + + @Mock + private TransactionPersistenceInterface persistence; + + private TransactionWriter writer; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + writer = new TransactionWriter(persistence); + } + + @Test + void writeTXToPersistence_ValidTransaction_TransactionSaved() { + // Arrange + Transaction transaction = new Transaction(); + transaction.setOfferId("123"); + transaction.setVolume(123f); + transaction.setEarnings(12.23); + transaction.setTransactionTime("2020-01-01 00:00:00"); + + TransactionDTO expectedDTO = TransactionMapper.mapTXToDTO(transaction); + + // Act + writer.writeTXToPersistence(transaction); + + // Assert + verify(persistence).writeTX(expectedDTO); + } + + @Test + void writeTXToPersistence_InvalidTransaction_ValidationExceptionThrown() { + // Arrange + Transaction transaction = new Transaction(); + transaction.setVolume(123f); + transaction.setEarnings(12.23); + transaction.setTransactionTime("2020-01-01 00:00:00"); + + // Act & Assert + assertThrows(InvalidTransactionException.class, () -> writer.writeTXToPersistence(transaction)); + + // Verify that persistence.writeTX is not called + verifyNoInteractions(persistence); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java index 1c7d4f9..cd1d9b9 100644 --- a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java +++ b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java @@ -1,5 +1,6 @@ package de.rwu.easydrop.util; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -29,7 +30,7 @@ class FormattingUtilTest { @Test void testFormatEuro_positiveAmount() { double amount = 1234.56; - String expectedFormattedAmount = "1.234,56 €"; + String expectedFormattedAmount = "1.234,56 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount); @@ -39,7 +40,7 @@ class FormattingUtilTest { @Test void testFormatEuro_zeroAmount() { double amount = 0.0; - String expectedFormattedAmount = "0,00 €"; + String expectedFormattedAmount = "0,00 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount); @@ -49,10 +50,23 @@ class FormattingUtilTest { @Test void testFormatEuro_negativeAmount() { double amount = -789.12; - String expectedFormattedAmount = "-789,12 €"; + String expectedFormattedAmount = "-789,12 Euro"; String formattedAmount = FormattingUtil.formatEuro(amount); Assertions.assertEquals(expectedFormattedAmount, formattedAmount); } + + @Test + void testRemoveSpaces_RemovesAllSpaces() { + String test1 = FormattingUtil.removeSpaces("this is a test"); + String test2 = FormattingUtil.removeSpaces("another test"); + String test3 = FormattingUtil.removeSpaces(" daring today, aren't we? "); + String test4 = FormattingUtil.removeSpaces("hehe"); + + assertEquals("thisisatest", test1); + assertEquals("anothertest", test2); + assertEquals("daringtoday,aren'twe?", test3); + assertEquals("hehe", test4); + } } diff --git a/src/test/resources/test.malformed.products-config.json b/src/test/resources/test.malformed.products-config.json index 00d0092..1d7c6fe 100644 --- a/src/test/resources/test.malformed.products-config.json +++ b/src/test/resources/test.malformed.products-config.json @@ -5,10 +5,10 @@ description: "Integration Testing Product", "identifiers": [ { - "Amazon": "DEMO-AMAZON-001" + "AMAZON": "DEMO-AMAZON-001" }, { - "eBay": "DEMO-EBAY-001" + "EBAY": "DEMO-EBAY-001" } ] ] diff --git a/src/test/resources/test.products-config.json b/src/test/resources/test.products-config.json index 44262c8..c48cfc6 100644 --- a/src/test/resources/test.products-config.json +++ b/src/test/resources/test.products-config.json @@ -5,10 +5,10 @@ "description": "Integration Testing Product", "identifiers": [ { - "Amazon": "DEMO-AMAZON-001" + "AMAZON": "DEMO-AMAZON-001" }, { - "eBay": "DEMO-EBAY-001" + "EBAY": "DEMO-EBAY-001" } ] }