diff --git a/.vscode/settings.json b/.vscode/settings.json index e0f15db..b72b3e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,8 @@ { - "java.configuration.updateBuildConfiguration": "automatic" -} \ No newline at end of file + "java.configuration.updateBuildConfiguration": "automatic", + "sonarlint.connectedMode.project": { + "connectionId": "LocalSonarQube", + "projectKey": "EasyDrop" + }, + "java.debug.settings.onBuildFailureProceed": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 010c0fd..4daf9c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,34 @@ # Changelog +## 0.2.0-SNAPSHOT + +### Added + +- `EbayItemDataSource` (#37) +- `Product` class +- Mapping between Product and its corresponding DTO +- Dependencies: Lombok for easier notation, JUnit 5 Params for repetitive tests +- Product Validation +- Product Retrieval +- Tests + +### Changed + +- `AbstractDataSource` between DataSource interface and implementations to minimize code duplication + - Affects AmazonProductDataSource and related tests (~ #39) +- Testing resources moved to more intuitive location +- Demo `Main` method now uses proper Product retrieval + +### Fixed + +- Logback configuration is now linked correctly +- Config now properly adheres to singleton pattern + ## 0.1.0 ### Added -- (DataSource) → AmazonProductDataSource as first external data source (#39) -- ProductDTO as data holder structure for data from external sources (~ #39) +- (DataSource) → `AmazonProductDataSource` as first external data source (#39) +- `ProductDTO` as data holder structure for data from external sources (~ #39) - Config for credential, base URLs etc. (~ #39) - FormattingUtil for formatting price strings (~ #39) diff --git a/SampleResponses/eBayProductAPISample.json b/SampleResponses/eBayProductAPISample.json new file mode 100644 index 0000000..510288b --- /dev/null +++ b/SampleResponses/eBayProductAPISample.json @@ -0,0 +1,133 @@ +{ + "href": "https://api.ebay.com/buy/browse/v1/item_summary/search?q=drone&limit=1&offset=0", + "total": 260202, + "next": "https://api.ebay.com/buy/browse/v1/item_summary/search?q=drone&limit=1&offset=1", + "limit": 3, + "offset": 0, + "itemSummaries": [ + { + "itemId": "v1|1**********1|0", + "title": "Syma X5SW-V3 Wifi FPV RC Drone Quadcopter 2.4Ghz 6-Axis Gyro with Headless Mode", + "leafCategoryIds": ["179697", "182186"], + "categories": [ + { + "categoryId": "179697", + "categoryName": "Camera Drones" + }, + { + "categoryId": "182186", + "categoryName": "Other RC Model Vehicles & Kits" + }, + { + "categoryId": "2562", + "categoryName": "Radio Control & Control Line" + }, + { + "categoryId": "220", + "categoryName": "Toys & Hobbies" + }, + { + "categoryId": "625", + "categoryName": "Cameras & Photo" + }, + { + "categoryId": "182181", + "categoryName": "RC Model Vehicles & Kits" + } + ], + "image": { + "imageUrl": "https://i.ebayimg.com/thumbs/images/g/n**************a/s-***5.jpg" + }, + "price": { + "value": "59.99", + "currency": "USD" + }, + "itemHref": "https://api.ebay.com/buy/browse/v1/item/v1******************0", + "seller": { + "username": "m********e", + "feedbackPercentage": "98.6", + "feedbackScore": 130000 + }, + "marketingPrice": { + "originalPrice": { + "value": "74.99", + "currency": "USD" + }, + "discountPercentage": "20", + "discountAmount": { + "value": "15.00", + "currency": "USD" + }, + "priceTreatment": "LIST_PRICE" + }, + "condition": "New", + "conditionId": "1000", + "thumbnailImages": [ + { + "imageUrl": "https://i.ebayimg.com/images/g/n**************a/s-l***0.jpg" + } + ], + "shippingOptions": [ + { + "shippingCostType": "FIXED", + "shippingCost": { + "value": "0.00", + "currency": "USD" + }, + "minEstimatedDeliveryDate": "2022-11-19T08:00:00.000Z", + "maxEstimatedDeliveryDate": "2022-11-21T08:00:00.000Z", + "guaranteedDelivery": true + } + ], + "buyingOptions": ["FIXED_PRICE", "BEST_OFFER"], + "itemAffiliateWebUrl": "https://www.ebay.com/itm/1**********1?hash=i************d*9", + "itemWebUrl": "https://www.ebay.com/itm/1**********1?hash=i************d*p", + "itemLocation": { + "postalCode": "0****", + "country": "US" + }, + "additionalImages": [ + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_2_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_3_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_4_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_5_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_6_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_7_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_8_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_9_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_10_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_11_0_1/225x225.jpg" + }, + { + "imageUrl": "https://origin-galleryplus.ebayimg.com/ws/web/1**********1_12_0_1/225x225.jpg" + } + ], + "adultOnly": false, + "legacyItemId": "1**********1", + "availableCoupons": false, + "itemCreationDate": "2022-12-25T07:14:44.000Z", + "topRatedBuyingExperience": true, + "priorityListing": true, + "listingMarketplaceId": "EBAY_US" + } + ] +} diff --git a/Script/SonarQube_Local.sh b/Script/SonarQube_Local.sh new file mode 100644 index 0000000..18b0b37 --- /dev/null +++ b/Script/SonarQube_Local.sh @@ -0,0 +1,5 @@ +mvn clean verify sonar:sonar -Pcoverage \ + -Dsonar.projectKey=EasyDrop \ + -Dsonar.projectName='EasyDrop' \ + -Dsonar.host.url=http://localhost:9000 \ + -Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931 \ No newline at end of file diff --git a/config/demo.config.properties b/config/demo.config.properties index fbb6e44..d3e4d63 100644 --- a/config/demo.config.properties +++ b/config/demo.config.properties @@ -1,3 +1,5 @@ # Amazon Credentials AMAZON_API_URL= -AMAZON_API_KEY= \ No newline at end of file +AMAZON_API_KEY= +EBAY_API_URL= +EBAY_API_KEY= \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..a23edb4 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/pom.xml b/pom.xml index 88f5f8e..0537059 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ de.rwu easydrop jar - 0.1.0 + 0.2.0-SNAPSHOT EasyDrop http://maven.apache.org @@ -16,16 +16,29 @@ + + org.projectlombok + lombok + 1.18.28 + provided + + org.junit.jupiter junit-jupiter-api - 5.8.0 + 5.9.3 test org.junit.jupiter junit-jupiter-engine - 5.8.0 + 5.9.3 + test + + + org.junit.jupiter + junit-jupiter-params + 5.9.3 test diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index af1aeea..74b6aec 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -5,7 +5,8 @@ import javax.naming.ConfigurationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import de.rwu.easydrop.api.client.AmazonProductDataSource; +import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.service.retriever.ProductRetriever; import de.rwu.easydrop.util.Config; /** @@ -33,17 +34,12 @@ public final class Main { */ public static void main(final String[] args) throws ConfigurationException { Config config = Config.getInstance(); - config.loadConfig(); - String amznBaseUrl = config.getProperty("AMAZON_API_URL"); - String amznApiKey = config.getProperty("AMAZON_API_KEY"); - String testProduct = null; + DataSourceFactory dataSourceFactory = new DataSourceFactory(config); + ProductRetriever retriever = new ProductRetriever(dataSourceFactory); - AmazonProductDataSource amznSrc = new AmazonProductDataSource(amznBaseUrl, amznApiKey); - try { - testProduct = amznSrc.getProductDTOById("B096Y2TYKV").toString(); - LOGGER.info(testProduct); - } catch (IllegalArgumentException e) { - LOGGER.error("Something went wrong :(", e); - } + String amznProduct = retriever.getProductFromAmazon("B096Y2TYKV").toString(); + LOGGER.info(amznProduct); + String ebayProduct = retriever.getProductFromEbay("Gigabyte GeForce RTX 3060").toString(); + LOGGER.info(ebayProduct); } } diff --git a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java new file mode 100644 index 0000000..7751c10 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java @@ -0,0 +1,100 @@ +package de.rwu.easydrop.api.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.DataSourceException; +import de.rwu.easydrop.util.FormattingUtil; + +/** + * Helper class for shared data and functions between data sources. + * + * @since 0.2.0 + */ +public abstract class AbstractDataSource implements DataSource { + + /** + * Returns the data origin for the current source. + * + * @return Data source name + */ + protected abstract String getDataOrigin(); + + /** + * Returns the data source's API key. + * + * @return Data source API key + */ + protected abstract String getApiKey(); + + /** + * Enriches a ProductDTO with API-gathered data. + * + * @param product Unfinished ProductDTO + * @param json Product data + * @return Finished ProductDTO + */ + protected abstract ProductDTO buildProductDTO(ProductDTO product, String json); + + /** + * Overridable standard implementation. + */ + @Override + public ProductDTO getProductDTOById(final String productIdentifier) + throws IllegalArgumentException { + StringBuilder response = new StringBuilder(); + String dataOrigin = getDataOrigin(); + String apiKey = getApiKey(); + ProductDTO product = new ProductDTO(productIdentifier, dataOrigin); + + try { + String urlReadyIdentifier = FormattingUtil.urlEncode(productIdentifier); + URL apiUrl = createApiUrl(urlReadyIdentifier); + + HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Credential", apiKey); + + int responseCode = connection.getResponseCode(); + BufferedReader reader; + if (responseCode == HttpURLConnection.HTTP_OK) { + reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + } else { + throw new DataSourceException( + "Nothing found: " + + dataOrigin + + " API responded with error code " + + responseCode); + } + + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + reader.close(); + + buildProductDTO(product, response.toString()); + } catch (IOException e) { + throw new DataSourceException( + "Couldn't fulfill " + + dataOrigin + + " API request"); + } + + return product; + } + + /** + * 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; +} 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 dbbdc6d..83b758d 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java @@ -1,9 +1,5 @@ package de.rwu.easydrop.api.client; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -18,7 +14,7 @@ import de.rwu.easydrop.api.dto.ProductDTO; * * @since 0.1.0 */ -public final class AmazonProductDataSource implements DataSource { +public final class AmazonProductDataSource extends AbstractDataSource { /** * Name of this data source. */ @@ -28,7 +24,7 @@ public final class AmazonProductDataSource implements DataSource { */ private String baseUrl; /** - * Credential key to authorize acccess. + * Credential key to authorize access. */ private String apiKey; /** @@ -52,48 +48,7 @@ public final class AmazonProductDataSource implements DataSource { } @Override - public ProductDTO getProductDTOById(final String productId) throws IllegalArgumentException { - StringBuilder response = new StringBuilder(); - ProductDTO product = new ProductDTO(productId, DATA_ORIGIN); - - try { - URL apiUrl = createApiUrl(productId); - - HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("Credential", apiKey); - - int responseCode = connection.getResponseCode(); - BufferedReader reader; - if (responseCode == HttpURLConnection.HTTP_OK) { - reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - } else { - throw new IllegalArgumentException( - "Nothing found: Amazon API responded with error code " + responseCode); - } - - String line; - while ((line = reader.readLine()) != null) { - response.append(line); - } - reader.close(); - - buildProductDTO(product, response.toString()); - } catch (IOException e) { - throw new IllegalArgumentException("Couldn't fulfill Amazon API request"); - } - - return product; - } - - /** - * Enriches a ProductDTO with API-gathered data. - * - * @param product Unfinished ProductDTO - * @param json Product data - * @return Finished ProductDTO - */ - public ProductDTO buildProductDTO(final ProductDTO product, final String json) { + protected ProductDTO buildProductDTO(final ProductDTO product, final String json) { String root = "$.featuredOffer."; ReadContext ctx = JsonPath.parse(json); @@ -112,8 +67,11 @@ public final class AmazonProductDataSource implements DataSource { return product; } + /** + * @param productId ASIN + */ @Override - public URL createApiUrl(final String productId) throws MalformedURLException { + protected URL createApiUrl(final String productId) throws MalformedURLException { return new URL(baseUrl + "/products/2020-08-26/products/" + productId @@ -122,4 +80,14 @@ public final class AmazonProductDataSource implements DataSource { + "&locale=" + LOCALE); } + + @Override + protected String getDataOrigin() { + return DATA_ORIGIN; + } + + @Override + protected String getApiKey() { + return this.apiKey; + } } diff --git a/src/main/java/de/rwu/easydrop/api/client/DataSource.java b/src/main/java/de/rwu/easydrop/api/client/DataSource.java index 7b1c4e7..bda028f 100644 --- a/src/main/java/de/rwu/easydrop/api/client/DataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/DataSource.java @@ -1,8 +1,5 @@ package de.rwu.easydrop.api.client; -import java.net.MalformedURLException; -import java.net.URL; - import de.rwu.easydrop.api.dto.ProductDTO; /** @@ -12,17 +9,8 @@ public interface DataSource { /** * Retrieves product info from the data source. * - * @param productId ASIN + * @param productIdentifier Product identifier * @return ProductDTO */ - ProductDTO getProductDTOById(String productId); - - /** - * Creates an URL object to connect to the API with. - * - * @param productId ASIN - * @return URL object - * @throws MalformedURLException - */ - URL createApiUrl(String productId) throws MalformedURLException; + ProductDTO getProductDTOById(String productIdentifier); } diff --git a/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java new file mode 100644 index 0000000..ba79ab5 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java @@ -0,0 +1,55 @@ +package de.rwu.easydrop.api.client; + +import javax.naming.ConfigurationException; + +import de.rwu.easydrop.util.Config; + +/** + * Factory for Data Sources. + * + * @since 0.2.0 + */ +public class DataSourceFactory { + + /** + * 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 DataSourceFactory(final Config newConfig) throws ConfigurationException { + this.setConfig(newConfig); + } + + /** + * Creates an Amazon Product Data Source. + * + * @return AmazonProductDataSource + */ + public AmazonProductDataSource createAmazonProductDataSource() { + String apiUrl = config.getProperty("AMAZON_API_URL"); + String apiKey = config.getProperty("AMAZON_API_KEY"); + return new AmazonProductDataSource(apiUrl, apiKey); + } + + /** + * Creates an eBay Item Data Source. + * + * @return EbayItemDataSource + */ + public EbayItemDataSource createEbayItemDataSource() { + String apiUrl = config.getProperty("EBAY_API_URL"); + String apiKey = config.getProperty("EBAY_API_KEY"); + return new EbayItemDataSource(apiUrl, apiKey); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java new file mode 100644 index 0000000..171b8b9 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java @@ -0,0 +1,88 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; +import com.jayway.jsonpath.ReadContext; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Interface to an eBay data source. + * + * @since 0.2.0 + */ +public final class EbayItemDataSource extends AbstractDataSource { + /** + * Name of this data source. + */ + private static final String DATA_ORIGIN = "eBay"; + /** + * Base URL to the eBay data source. + */ + private String baseUrl; + /** + * Credential key to authorize access. + */ + private String apiKey; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public EbayItemDataSource(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + /** + * @param searchQuery Exact product name or other valid identifier. + */ + @Override + protected URL createApiUrl(final String searchQuery) throws MalformedURLException { + return new URL(baseUrl + + "/buy/browse/v1/item_summary/search?q=" + + searchQuery + + "&limit=1&offset=0"); + } + + /** + * Enriches a ProductDTO with API-gathered data. + * + * @param product Unfinished ProductDTO + * @param json Product data + * @return Finished ProductDTO + */ + protected ProductDTO buildProductDTO(final ProductDTO product, final String json) { + String root = "$.itemSummaries[0]."; + ReadContext ctx = JsonPath.parse(json); + + try { + product.setDataOrigin(DATA_ORIGIN); + product.setAvailable( + ctx.read(root + "shippingOptions[0].guaranteedDelivery", boolean.class)); + product.setCurrentPrice(ctx.read(root + "price.value", double.class)); + product.setDeliveryPrice( + ctx.read(root + "shippingOptions[0].shippingCost.value", double.class)); + product.setMerchant(ctx.read(root + "seller.username", String.class)); + } catch (PathNotFoundException e) { + // Pass, allow incomplete ProductDTO to pass for later validation + } + + return product; + } + + @Override + protected String getDataOrigin() { + return DATA_ORIGIN; + } + + @Override + protected String getApiKey() { + return this.apiKey; + } +} 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 e3a706c..f6beb85 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java @@ -1,139 +1,52 @@ package de.rwu.easydrop.api.dto; -import de.rwu.easydrop.util.FormattingUtil; +import lombok.Data; /** * Product data transfer object. * * @since 0.1.0 */ +@Data public class ProductDTO { /** * Data source platform, like "Amazon". */ private String dataOrigin; - /** - * @return the dataOrigin - */ - public String getDataOrigin() { - return dataOrigin; - } - - /** - * @param newDataOrigin the dataOrigin to set - */ - public void setDataOrigin(final String newDataOrigin) { - this.dataOrigin = newDataOrigin; - } - /** * Platform internal product identifier. */ private String productId; - /** - * @return the productId - */ - public String getProductId() { - return productId; - } - /** * Current product price per piece in Euro. */ private double currentPrice; - /** - * @return the currentPrice - */ - public double getCurrentPrice() { - return currentPrice; - } - - /** - * @param newCurrentPrice the currentPrice to set - */ - public void setCurrentPrice(final double newCurrentPrice) { - this.currentPrice = newCurrentPrice; - } - /** * Name of mercant offering the product on the platform. */ private String merchant; - /** - * @return the merchant - */ - public String getMerchant() { - return merchant; - } - - /** - * @param newMerchant the merchant to set - */ - public void setMerchant(final String newMerchant) { - this.merchant = newMerchant; - } - /** * Additional Cost for delivery in Euro. */ private double deliveryPrice; - /** - * @return the deliveryPrice - */ - public double getDeliveryPrice() { - return deliveryPrice; - } - - /** - * @param newDeliveryPrice the deliveryPrice to set - */ - public void setDeliveryPrice(final double newDeliveryPrice) { - this.deliveryPrice = newDeliveryPrice; - } - /** * Whether the product can be purchased at this point. */ private boolean available; - /** - * @return the available - */ - public boolean isAvailable() { - return available; - } - - /** - * @param newAvailable the available to set - */ - public void setAvailable(final boolean newAvailable) { - this.available = newAvailable; - } - /** * Creates ProductDTO instance. * - * @param newProductId Interal Product indetifier + * @param newProductId Internal Product indetifier * @param newDataOrigin Data Origin */ public ProductDTO(final String newProductId, final String newDataOrigin) { this.productId = newProductId; this.dataOrigin = newDataOrigin; } - - @Override - public final String toString() { - return "ProductDTO{" - + productId + " from " - + merchant + " (" - + dataOrigin + ")" - + " at " - + FormattingUtil.formatEuro(currentPrice) + " (available: " - + (available ? "yes" : "no") + ")}"; - } } diff --git a/src/main/java/de/rwu/easydrop/data/model/Product.java b/src/main/java/de/rwu/easydrop/data/model/Product.java deleted file mode 100644 index cfee872..0000000 --- a/src/main/java/de/rwu/easydrop/data/model/Product.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.data.model; - -/** - * A Product. - * - * TODO implement - */ -public class Product { - -} diff --git a/src/main/java/de/rwu/easydrop/data/model/package-info.java b/src/main/java/de/rwu/easydrop/data/model/package-info.java deleted file mode 100644 index f33ce31..0000000 --- a/src/main/java/de/rwu/easydrop/data/model/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Business objects. - * - * TODO implement - */ -package de.rwu.easydrop.data.model; diff --git a/src/main/java/de/rwu/easydrop/exception/DataSourceException.java b/src/main/java/de/rwu/easydrop/exception/DataSourceException.java new file mode 100644 index 0000000..0bca4d8 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/DataSourceException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies the data of a Product are invalid. + * + * @since 0.2.0 + */ +public class DataSourceException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public DataSourceException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public DataSourceException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/exception/InvalidProductException.java b/src/main/java/de/rwu/easydrop/exception/InvalidProductException.java new file mode 100644 index 0000000..d42ba39 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/InvalidProductException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies the data of a Product are invalid. + * + * @since 0.2.0 + */ +public class InvalidProductException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public InvalidProductException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public InvalidProductException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/exception/package-info.java b/src/main/java/de/rwu/easydrop/exception/package-info.java new file mode 100644 index 0000000..327d7d1 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains EasyDrop-related custom exceptions. + * + * @since 0.2.0 + */ +package de.rwu.easydrop.exception; diff --git a/src/main/java/de/rwu/easydrop/model/Product.java b/src/main/java/de/rwu/easydrop/model/Product.java new file mode 100644 index 0000000..1e5b3b2 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/Product.java @@ -0,0 +1,53 @@ +package de.rwu.easydrop.model; + +import de.rwu.easydrop.util.FormattingUtil; +import lombok.Data; + +/** + * A Product. + * + * @since 0.2.0 + */ +@Data +public class Product { + /** + * Data source platform, like "Amazon". + */ + private String dataOrigin; + + /** + * Platform internal product identifier. + */ + private String productId; + + /** + * Current product price per piece in Euro. + */ + private double currentPrice; + + /** + * Name of mercant offering the product on the platform. + */ + private String merchant; + + /** + * Additional Cost for delivery in Euro. + */ + private double deliveryPrice; + + /** + * Whether the product can be purchased at this point. + */ + private boolean available; + + @Override + public final String toString() { + return "Product: [" + + productId + " from " + + merchant + " (" + + dataOrigin + ")" + + " at " + + FormattingUtil.formatEuro(currentPrice) + " (available: " + + (available ? "yes" : "no") + ")]"; + } +} diff --git a/src/main/java/de/rwu/easydrop/model/package-info.java b/src/main/java/de/rwu/easydrop/model/package-info.java new file mode 100644 index 0000000..0c255df --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/package-info.java @@ -0,0 +1,6 @@ +/** + * Business objects. + * + * @since 0.2.0 + */ +package de.rwu.easydrop.model; 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 5d4a6d3..1b37a11 100644 --- a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java +++ b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java @@ -1,14 +1,44 @@ package de.rwu.easydrop.service.mapping; +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.model.Product; + /** * Maps between Product, ProductDAO and ProductDTO. * - * TODO implement + * @since 0.2.0 * * @see Product * @see ProductDTO * @see ProductDAO */ -public class ProductMapper { +public final class ProductMapper { + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private ProductMapper() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a mapping class, don't instantiate it."); + } + + /** + * Creates a Product object from a corresponding DTO. + * + * @param dto Product Data Transfer Object + * @return Product + */ + public static Product mapProductFromDTO(final ProductDTO dto) { + Product product = new Product(); + + product.setAvailable(dto.isAvailable()); + product.setCurrentPrice(dto.getCurrentPrice()); + product.setDataOrigin(dto.getDataOrigin()); + product.setDeliveryPrice(dto.getDeliveryPrice()); + product.setMerchant(dto.getMerchant()); + product.setProductId(dto.getProductId()); + + return product; + } } diff --git a/src/main/java/de/rwu/easydrop/service/mapping/package-info.java b/src/main/java/de/rwu/easydrop/service/mapping/package-info.java index ab86051..944f66d 100644 --- a/src/main/java/de/rwu/easydrop/service/mapping/package-info.java +++ b/src/main/java/de/rwu/easydrop/service/mapping/package-info.java @@ -1,6 +1,6 @@ /** * Maps different formats of corresponding objects. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.service.mapping; diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java new file mode 100644 index 0000000..bce5d10 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -0,0 +1,67 @@ +package de.rwu.easydrop.service.retriever; + +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.model.Product; +import de.rwu.easydrop.service.mapping.ProductMapper; +import de.rwu.easydrop.service.validation.ProductValidator; + +/** + * Retrieves Product Objects from various data sources. + * + * @since 0.2.0 + */ +public class ProductRetriever { + /** + * Data source factory. + */ + private DataSourceFactory dataSourceFactory; + + /** + * @param newDataSourceFactory the dataSourceFactory to set + */ + public void setDataSourceFactory(final DataSourceFactory newDataSourceFactory) { + this.dataSourceFactory = newDataSourceFactory; + } + + /** + * @param newDataSourceFactory + */ + public ProductRetriever(final DataSourceFactory newDataSourceFactory) { + this.setDataSourceFactory(newDataSourceFactory); + } + + /** + * Retrieves a product from Amazon. + * + * @param asin Product identifier + * @return Product from Amazon + */ + public Product getProductFromAmazon(final String asin) { + AmazonProductDataSource dataSource = dataSourceFactory.createAmazonProductDataSource(); + ProductDTO dto = dataSource.getProductDTOById(asin); + + Product product = ProductMapper.mapProductFromDTO(dto); + ProductValidator.validate(product); + + return product; + } + + /** + * Retrieves a product from eBay. + * + * @param query Product search query + * @return Product from eBay + */ + public Product getProductFromEbay(final String query) { + EbayItemDataSource dataSource = dataSourceFactory.createEbayItemDataSource(); + ProductDTO dto = dataSource.getProductDTOById(query); + + Product product = ProductMapper.mapProductFromDTO(dto); + ProductValidator.validate(product); + + return product; + } +} diff --git a/src/main/java/de/rwu/easydrop/service/retriever/package-info.java b/src/main/java/de/rwu/easydrop/service/retriever/package-info.java new file mode 100644 index 0000000..f412441 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/retriever/package-info.java @@ -0,0 +1,6 @@ +/** + * Retrieves Objects from a data source. + * + * @since 0.2.0 + */ +package de.rwu.easydrop.service.retriever; 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 3c93108..fc48377 100644 --- a/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java +++ b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java @@ -1,10 +1,56 @@ 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; + /** * Confirms validity of Product data. * - * @since 0.1.0 + * @since 0.2.0 */ -public class ProductValidator { +public final class ProductValidator { + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private ProductValidator() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a validator class, don't instantiate it."); + } + + /** + * Makes sure a Product does not contain invalid information. + * + * @param product the Product + */ + public static void validate(final Product product) { + 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/package-info.java b/src/main/java/de/rwu/easydrop/service/validation/package-info.java index 6b45561..a74f45c 100644 --- a/src/main/java/de/rwu/easydrop/service/validation/package-info.java +++ b/src/main/java/de/rwu/easydrop/service/validation/package-info.java @@ -1,6 +1,6 @@ /** * Supports validation processes. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.service.validation; diff --git a/src/main/java/de/rwu/easydrop/util/Config.java b/src/main/java/de/rwu/easydrop/util/Config.java index 7a7f1e3..32cce6a 100644 --- a/src/main/java/de/rwu/easydrop/util/Config.java +++ b/src/main/java/de/rwu/easydrop/util/Config.java @@ -56,7 +56,7 @@ public final class Config { */ public static Config getInstance() { if (instance == null) { - return new Config(); + instance = new Config(); } return instance; @@ -109,4 +109,11 @@ public final class Config { public void setProperty(final String key, final String value) { properties.setProperty(key, value); } + + /** + * Resets the config's properties. + */ + public void reset() { + properties = null; + } } diff --git a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java index 283e506..7ef0e69 100644 --- a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java +++ b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java @@ -27,4 +27,14 @@ public final class FormattingUtil { public static String formatEuro(final double amount) { return String.format(Locale.GERMAN, "%,.2f", amount) + " €"; } + + /** + * Makes a string URL ready. For now, only spaces are replaced. + * + * @param str + * @return URL-ready string + */ + public static String urlEncode(final String str) { + return str.replace(" ", "+"); + } } diff --git a/config/logback.xml b/src/main/resources/logback.xml similarity index 80% rename from config/logback.xml rename to src/main/resources/logback.xml index 6b610a9..a2b1b43 100644 --- a/config/logback.xml +++ b/src/main/resources/logback.xml @@ -1,4 +1,6 @@ + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n @@ -6,7 +8,7 @@ - + \ No newline at end of file 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 032d682..938413d 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.DataSourceException; class AmazonProductDataSourceTest { @@ -150,6 +151,7 @@ class AmazonProductDataSourceTest { 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(); HttpURLConnection mockConnection = mock(HttpURLConnection.class); @@ -157,7 +159,7 @@ class AmazonProductDataSourceTest { when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); // Invoke the method and verify the exception - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + DataSourceException exception = assertThrows(DataSourceException.class, () -> { dataSource.getProductDTOById(demoProductId); }); @@ -170,6 +172,7 @@ class AmazonProductDataSourceTest { // Set up the test environment 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(); @@ -179,11 +182,23 @@ class AmazonProductDataSourceTest { when(mockConnection.getInputStream()).thenThrow(new IOException()); // Invoke the method and verify the exception - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + DataSourceException exception = assertThrows(DataSourceException.class, () -> { dataSource.getProductDTOById(demoProductId); }); // Verify the exception message assertEquals("Couldn't fulfill Amazon API request", exception.getMessage()); } + + @Test + void getDataOrigin_ReturnsExpectedDataOrigin() { + String dataOrigin = demoDataSource.getDataOrigin(); + assertEquals(demoDataOrigin, dataOrigin); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoDataSource.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 new file mode 100644 index 0000000..c84df2a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java @@ -0,0 +1,48 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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 DataSourceFactoryTest { + @Mock + private Config config; + + private DataSourceFactory dataSourceFactory; + + @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"); + 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); + } + + @Test + void createAmazonProductDataSource_ReturnsAmazonProductDataSource() { + // Act + AmazonProductDataSource dataSource = dataSourceFactory.createAmazonProductDataSource(); + + // Assert + assertEquals("amazon-api-key", dataSource.getApiKey()); + } + + @Test + void createEbayItemDataSource_ReturnsEbayItemDataSource() { + // Act + EbayItemDataSource dataSource = dataSourceFactory.createEbayItemDataSource(); + + // Assert + 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 new file mode 100644 index 0000000..9a6ed45 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java @@ -0,0 +1,125 @@ +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.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +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.api.dto.ProductDTO; +import de.rwu.easydrop.exception.DataSourceException; + +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 String demoQuery = "iPhone"; + + @BeforeEach + void setUp() { + demoDataSource = new EbayItemDataSource(demoApiUrl, demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = EbayItemDataSource.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoDataSource)); + + Field apiKeyField = EbayItemDataSource.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoDataSource)); + } catch (NoSuchFieldException e) { + Assertions.fail(); + } catch (IllegalAccessException e) { + Assertions.fail(); + } + } + + @Test + void createApiUrl_ValidSearchQuery_ReturnsValidUrl() throws MalformedURLException { + URL apiUrl = demoDataSource.createApiUrl(demoQuery); + + assertNotNull(apiUrl); + assertEquals("https://www.example.com/api/buy/browse/v1/item_summary/search?q=iPhone&limit=1&offset=0", + apiUrl.toString()); + } + + @Test + void buildProductDTO_ValidJson_ReturnsValidProductDTO() { + ProductDTO product = new ProductDTO(demoQuery, demoDataOrigin); + String json = "{\"itemSummaries\":[{\"shippingOptions\":[{\"guaranteedDelivery\":true,\"shippingCost\":{\"value\":10}}],\"price\":{\"value\":999.99},\"seller\":{\"username\":\"seller123\"}}]}"; + + ProductDTO result = demoDataSource.buildProductDTO(product, json); + + assertEquals(demoDataOrigin, result.getDataOrigin()); + assertEquals(true, result.isAvailable()); + assertEquals(999.99, result.getCurrentPrice()); + assertEquals(10.0, result.getDeliveryPrice()); + assertEquals("seller123", result.getMerchant()); + } + + @Test + void buildProductDTO_InvalidJson_ReturnsProductDTOWithDefaults() { + ProductDTO product = new ProductDTO(demoQuery, demoDataOrigin); + String json = "{\"itemSummaries\":[]}"; // Empty JSON to simulate missing data + + ProductDTO result = demoDataSource.buildProductDTO(product, json); + + assertEquals("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 + assertEquals(null, result.getMerchant()); // Default value for String + } + + @Test + void getDataOrigin_ReturnsExpectedDataOrigin() { + String dataOrigin = demoDataSource.getDataOrigin(); + assertEquals(demoDataOrigin, dataOrigin); + } + + @Test + void getApiKey_ReturnsExpectedApiKey() { + String apiKey = demoDataSource.getApiKey(); + assertEquals(demoApiKey, apiKey); + } + + @Test + void testGetProductDTOById_failedRequest() throws IOException { + // Set up the test environment + + 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(); + 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); + }); + + // Verify the exception message + assertEquals("Nothing found: eBay API responded with error code 404", exception.getMessage()); + } +} 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 9d5917a..1e16d7c 100644 --- a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java +++ b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java @@ -1,33 +1,56 @@ package de.rwu.easydrop.api.dto; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; class ProductDTOTest { + @Test - void testToString1() { - ProductDTO product1 = new ProductDTO("12345", "Amazon"); - product1.setMerchant("Merchant A"); - product1.setCurrentPrice(19.99); - product1.setAvailable(true); + void constructor_SetsProductIdAndDataOrigin() { + // Arrange + String productId = "12345"; + String dataOrigin = "Amazon"; - String expectedString1 = "ProductDTO{12345 from Merchant A (Amazon) at 19,99 € (available: yes)}"; - String result1 = product1.toString(); + // Act + ProductDTO productDTO = new ProductDTO(productId, dataOrigin); - assertEquals(expectedString1, result1); + // Assert + assertEquals(productId, productDTO.getProductId()); + assertEquals(dataOrigin, productDTO.getDataOrigin()); } @Test - void testToString2() { - ProductDTO product2 = new ProductDTO("67890", "eBay"); - product2.setMerchant("Merchant B"); - product2.setCurrentPrice(9.99); - product2.setAvailable(false); + void gettersAndSetters_WorkAsExpected() { + // Arrange + ProductDTO productDTO = new ProductDTO("12345", "Amazon"); - String expectedString2 = "ProductDTO{67890 from Merchant B (eBay) at 9,99 € (available: no)}"; - String result2 = product2.toString(); + // Act and Assert + assertEquals("12345", productDTO.getProductId()); + assertEquals("Amazon", productDTO.getDataOrigin()); - assertEquals(expectedString2, result2); + // Modify fields + productDTO.setProductId("54321"); + productDTO.setDataOrigin("eBay"); + + // Assert + assertEquals("54321", productDTO.getProductId()); + assertEquals("eBay", productDTO.getDataOrigin()); + } + + @Test + void defaultConstructor_SetsDefaultValues() { + // Act + ProductDTO productDTO = new ProductDTO(null, null); + + // Assert + assertNull(productDTO.getProductId()); + assertNull(productDTO.getDataOrigin()); + assertEquals(0.0, productDTO.getCurrentPrice()); + assertNull(productDTO.getMerchant()); + assertEquals(0.0, productDTO.getDeliveryPrice()); + assertFalse(productDTO.isAvailable()); } } diff --git a/src/test/java/de/rwu/easydrop/exception/DataSourceExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/DataSourceExceptionTest.java new file mode 100644 index 0000000..930e1bc --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/DataSourceExceptionTest.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 DataSourceExceptionTest { + + @Test + void constructor_WithMessage_SetsMessage() { + // Arrange + String message = "Data source error"; + + // Act + DataSourceException exception = new DataSourceException(message); + + // Assert + assertEquals(message, exception.getMessage()); + } + + @Test + void constructor_WithMessageAndCause_SetsMessageAndCause() { + // Arrange + String message = "Data source error"; + Throwable cause = new IllegalArgumentException("Invalid argument"); + + // Act + DataSourceException exception = new DataSourceException(message, cause); + + // Assert + assertEquals(message, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } + + @Test + void constructor_WithNullMessage_SetsNullMessage() { + // Act + DataSourceException exception = new DataSourceException(null); + + // Assert + assertEquals(null, exception.getMessage()); + } + + @Test + void constructor_WithNullCause_SetsNullCause() { + // Act + DataSourceException exception = new DataSourceException("Data source error", null); + + // Assert + assertEquals(null, exception.getCause()); + } + + @Test + void throw_DataSourceException() { + // Act and Assert + assertThrows(DataSourceException.class, () -> { + throw new DataSourceException("Data source error"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/exception/InvalidProductExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/InvalidProductExceptionTest.java new file mode 100644 index 0000000..478e8e3 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/InvalidProductExceptionTest.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 InvalidProductExceptionTest { + + @Test + void constructor_WithMessage_SetsMessage() { + // Arrange + String message = "Invalid product data"; + + // Act + InvalidProductException exception = new InvalidProductException(message); + + // Assert + assertEquals(message, exception.getMessage()); + } + + @Test + void constructor_WithMessageAndCause_SetsMessageAndCause() { + // Arrange + String message = "Invalid product data"; + Throwable cause = new IllegalArgumentException("Invalid argument"); + + // Act + InvalidProductException exception = new InvalidProductException(message, cause); + + // Assert + assertEquals(message, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } + + @Test + void constructor_WithNullMessage_SetsNullMessage() { + // Act + InvalidProductException exception = new InvalidProductException(null); + + // Assert + assertEquals(null, exception.getMessage()); + } + + @Test + void constructor_WithNullCause_SetsNullCause() { + // Act + InvalidProductException exception = new InvalidProductException("Invalid product data", null); + + // Assert + assertEquals(null, exception.getCause()); + } + + @Test + void throw_InvalidProductException() { + // Act and Assert + assertThrows(InvalidProductException.class, () -> { + throw new InvalidProductException("Invalid product data"); + }); + } +} diff --git a/src/test/java/de/rwu/easydrop/model/ProductTest.java b/src/test/java/de/rwu/easydrop/model/ProductTest.java new file mode 100644 index 0000000..1845b65 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/model/ProductTest.java @@ -0,0 +1,75 @@ +package de.rwu.easydrop.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ProductTest { + @Test + void testToString1() { + Product product1 = new Product(); + product1.setDataOrigin("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 result1 = product1.toString(); + + assertEquals(expectedString1, result1); + } + + @Test + void testToString2() { + Product product2 = new Product(); + product2.setDataOrigin("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 result2 = product2.toString(); + + assertEquals(expectedString2, result2); + } + + @Test + void gettersAndSetters_WorkAsExpected() { + // Arrange + Product product = new Product(); + product.setDataOrigin("Amazon"); + product.setProductId("12345"); + product.setCurrentPrice(9.99); + product.setMerchant("Example Merchant"); + product.setDeliveryPrice(2.50); + product.setAvailable(true); + + // Act and Assert + assertEquals("Amazon", product.getDataOrigin()); + assertEquals("12345", product.getProductId()); + assertEquals(9.99, product.getCurrentPrice()); + assertEquals("Example Merchant", product.getMerchant()); + assertEquals(2.50, product.getDeliveryPrice()); + assertTrue(product.isAvailable()); + + // Modify fields + product.setDataOrigin("eBay"); + product.setProductId("54321"); + product.setCurrentPrice(19.99); + product.setMerchant("New Merchant"); + product.setDeliveryPrice(3.50); + product.setAvailable(false); + + // Assert + assertEquals("eBay", product.getDataOrigin()); + assertEquals("54321", product.getProductId()); + assertEquals(19.99, product.getCurrentPrice()); + assertEquals("New Merchant", product.getMerchant()); + assertEquals(3.50, product.getDeliveryPrice()); + assertFalse(product.isAvailable()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java new file mode 100644 index 0000000..6dbf741 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java @@ -0,0 +1,56 @@ +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.ProductDTO; +import de.rwu.easydrop.model.Product; + +class ProductMapperTest { + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = ProductMapper.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + @Test + void mapProductFromDTO_ReturnsProductWithMappedFields() { + // Arrange + ProductDTO dto = createProductDTO(); + + // Act + Product product = ProductMapper.mapProductFromDTO(dto); + + // Assert + assertEquals(dto.isAvailable(), product.isAvailable()); + assertEquals(dto.getCurrentPrice(), product.getCurrentPrice()); + assertEquals(dto.getDataOrigin(), product.getDataOrigin()); + assertEquals(dto.getDeliveryPrice(), product.getDeliveryPrice()); + assertEquals(dto.getMerchant(), product.getMerchant()); + assertEquals(dto.getProductId(), product.getProductId()); + } + + private ProductDTO createProductDTO() { + ProductDTO dto = new ProductDTO("12345", "Amazon"); + dto.setAvailable(true); + dto.setCurrentPrice(9.99); + dto.setDeliveryPrice(2.50); + dto.setMerchant("Example Merchant"); + return dto; + } +} diff --git a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java new file mode 100644 index 0000000..36dff9b --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java @@ -0,0 +1,87 @@ +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.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.ProductDTO; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.util.Config; + +class ProductRetrieverTest { + @Mock + private Config config; + @Mock + private DataSourceFactory dataSourceFactory; + @Mock + private AmazonProductDataSource amazonDataSource; + @Mock + private EbayItemDataSource ebayDataSource; + @Mock + private ProductDTO productDTO; + @Mock + private Product product; + + private ProductRetriever productRetriever; + + @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); + productRetriever = new ProductRetriever(dataSourceFactory); + } + + @Test + void getProductFromAmazon_ReturnsProduct() { + // Arrange + String asin = "B01234ABC"; + when(dataSourceFactory.createAmazonProductDataSource()).thenReturn(amazonDataSource); + when(amazonDataSource.getProductDTOById(asin)).thenReturn(productDTO); + when(productDTO.getProductId()).thenReturn(asin); + when(productDTO.getCurrentPrice()).thenReturn(9.99); + when(productDTO.getDataOrigin()).thenReturn("Amazon"); + + // Act + Product result = productRetriever.getProductFromAmazon(asin); + + // Assert + assertNotNull(result); + assertEquals(asin, result.getProductId()); + assertEquals(9.99, result.getCurrentPrice()); + verify(amazonDataSource, times(1)).getProductDTOById(asin); + } + + @Test + void getProductFromEbay_ReturnsProduct() { + // Arrange + String productQuery = "MySearchQuery"; + when(dataSourceFactory.createEbayItemDataSource()).thenReturn(ebayDataSource); + when(ebayDataSource.getProductDTOById(productQuery)).thenReturn(productDTO); + when(productDTO.getProductId()).thenReturn(productQuery); + when(productDTO.getCurrentPrice()).thenReturn(9.99); + when(productDTO.getDataOrigin()).thenReturn("eBay"); + + // Act + Product result = productRetriever.getProductFromEbay(productQuery); + + // Assert + assertNotNull(result); + assertEquals(productQuery, result.getProductId()); + assertEquals(9.99, result.getCurrentPrice()); + verify(ebayDataSource, times(1)).getProductDTOById(productQuery); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java b/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java new file mode 100644 index 0000000..7827bbb --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java @@ -0,0 +1,109 @@ +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; + +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.InvalidProductException; +import de.rwu.easydrop.model.Product; + +class ProductValidatorTest { + + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = ProductValidator.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_ValidProduct_NoExceptionThrown() { + // Arrange + Product product = new Product(); + product.setCurrentPrice(9.99); + product.setDataOrigin("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) { + // Act and Assert + assertThrows(InvalidProductException.class, () -> ProductValidator.validate(product)); + } + + 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.setProductId("12345"); + return product; + } + + private static Product createProductWithEmptyProductId() { + Product product = new Product(); + product.setCurrentPrice(9.99); + product.setDataOrigin("Amazon"); + product.setProductId(""); + return product; + } +} diff --git a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java index db7de79..59cece7 100644 --- a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java +++ b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java @@ -1,6 +1,7 @@ package de.rwu.easydrop.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; @@ -13,7 +14,7 @@ import org.junit.jupiter.api.Test; class ConfigImplTest { private Config config; - private final static String TESTDATA_PATH = "testResources/testdata.properties"; + private final static String TESTDATA_PATH = "src/test/resources/testdata.properties"; private final static String TESTDATA_KEY = "API_KEY"; private final static String TESTDATA_VAL = "keyIsHere"; @@ -34,7 +35,9 @@ class ConfigImplTest { } @Test - void testGetProperty_ConfigNotLoaded() { + void testGetProperty_ConfigNotLoaded() throws Exception { + config.reset(); + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> { config.getProperty(TESTDATA_KEY); }); @@ -74,7 +77,7 @@ class ConfigImplTest { @Test void testLoadConfigSuccessfully() { try { - config.setConfigLocation("testResources/testdata.properties"); + config.setConfigLocation("src/test/resources/testdata.properties"); config.loadConfig(); assertEquals(TESTDATA_VAL, config.getProperty(TESTDATA_KEY)); } catch (ConfigurationException e) { @@ -92,4 +95,19 @@ class ConfigImplTest { assertEquals("Couldn't load required config file", exception.getMessage()); } + + @Test + void testReset() throws ConfigurationException { + config.setConfigLocation("src/test/resources/testdata.properties"); + config.loadConfig(); + + assertNotNull(config.getProperty(TESTDATA_KEY)); + config.reset(); + + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> { + config.getProperty(TESTDATA_KEY); + }); + + assertEquals("Config has not been loaded", exception.getMessage()); + } } diff --git a/src/test/java/de/rwu/easydrop/util/ConfigTest.java b/src/test/java/de/rwu/easydrop/util/ConfigTest.java index 8cf0c2a..2b08b0a 100644 --- a/src/test/java/de/rwu/easydrop/util/ConfigTest.java +++ b/src/test/java/de/rwu/easydrop/util/ConfigTest.java @@ -2,6 +2,7 @@ package de.rwu.easydrop.util; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.spy; import javax.naming.ConfigurationException; @@ -20,17 +21,18 @@ class ConfigTest { } @Test - void testGetInstanceNull() { - config = null; - + void testGetInstance() { Config newConfig = Config.getInstance(); assertNotNull(newConfig); } @Test - void testGetInstanceNotNull() { - Config newConfig = Config.getInstance(); - assertNotNull(newConfig); + void testGetInstanceEquality() { + // Create "two" instances to check validity of Singleton pattern + Config instance1 = Config.getInstance(); + Config instance2 = Config.getInstance(); + + assertSame(instance1, instance2, "Instances should be equal"); } @Test diff --git a/src/test/resources/empty.properties b/src/test/resources/empty.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/test/resources/empty.properties @@ -0,0 +1 @@ + diff --git a/src/test/resources/testdata.properties b/src/test/resources/testdata.properties new file mode 100644 index 0000000..71176b3 --- /dev/null +++ b/src/test/resources/testdata.properties @@ -0,0 +1,5 @@ +API_KEY=keyIsHere +AMAZON_API_URL=THE_AMAZON_API_URL +AMAZON_API_KEY=THE_AMAZON_API_KEY +EBAY_API_URL=THE_EBAY_API_URL +EBAY_API_KEY=THE_EBAY_API_KEY diff --git a/testResources/empty.properties b/testResources/empty.properties deleted file mode 100644 index e69de29..0000000 diff --git a/testResources/testdata.properties b/testResources/testdata.properties deleted file mode 100644 index cbf5875..0000000 --- a/testResources/testdata.properties +++ /dev/null @@ -1 +0,0 @@ -API_KEY=keyIsHere \ No newline at end of file