From 039eed43e7db19249c7aa95e593a2c486813a50a Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 31 May 2023 01:02:40 +0200 Subject: [PATCH 01/37] Bump version to 0.2.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 88f5f8e..6a81bd6 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 From 4f821c0a9c484dd60b01ba1a0f3b3fdc53010028 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 31 May 2023 01:03:06 +0200 Subject: [PATCH 02/37] #37 Added json structure reference --- SampleResponses/eBayProductAPISample.json | 133 ++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 SampleResponses/eBayProductAPISample.json 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" + } + ] +} From 61087270174e387e9d5f8ebb521330e88c1bf551 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 31 May 2023 11:48:02 +0200 Subject: [PATCH 03/37] Fixed typo --- .../de/rwu/easydrop/api/client/AmazonProductDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..d8c5d3e 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java @@ -28,7 +28,7 @@ public final class AmazonProductDataSource implements DataSource { */ private String baseUrl; /** - * Credential key to authorize acccess. + * Credential key to authorize access. */ private String apiKey; /** From ac7ee9adf118cbae8e659ecf6d98f29f6ef100ce Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 31 May 2023 13:23:40 +0200 Subject: [PATCH 04/37] Moved logback config to working location --- {config => src/main/resources}/logback.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename {config => src/main/resources}/logback.xml (80%) 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 From 59109220e67668d3e6f52d792baa5b2dbfbae85e Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 31 May 2023 13:46:00 +0200 Subject: [PATCH 05/37] Moved test files to better location --- .../de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java | 2 ++ src/test/resources/empty.properties | 1 + src/test/resources/testdata.properties | 1 + testResources/empty.properties | 0 testResources/testdata.properties | 1 - 5 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/empty.properties create mode 100644 src/test/resources/testdata.properties delete mode 100644 testResources/empty.properties delete mode 100644 testResources/testdata.properties 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..aa319af 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java @@ -150,6 +150,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); @@ -170,6 +171,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(); 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..854257f --- /dev/null +++ b/src/test/resources/testdata.properties @@ -0,0 +1 @@ +API_KEY=keyIsHere 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 From 2f1bd4407c0c6c4d4d0ac62c2052b35ae249eec7 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 31 May 2023 13:47:08 +0200 Subject: [PATCH 06/37] Added abstraction layer to DataSources --- .../api/client/AbstractDataSource.java | 99 +++++++++++++++++++ .../api/client/AmazonProductDataSource.java | 60 +++-------- .../rwu/easydrop/api/client/DataSource.java | 16 +-- .../de/rwu/easydrop/util/FormattingUtil.java | 10 ++ 4 files changed, 125 insertions(+), 60 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java 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..9d1df1c --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java @@ -0,0 +1,99 @@ +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.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 + */ + public 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 IllegalArgumentException( + "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 IllegalArgumentException( + "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 d8c5d3e..5654765 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. */ @@ -52,47 +48,6 @@ 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) { String root = "$.featuredOffer."; ReadContext ctx = JsonPath.parse(json); @@ -112,6 +67,9 @@ public final class AmazonProductDataSource implements DataSource { return product; } + /** + * @param productId ASIN + */ @Override public URL createApiUrl(final String productId) throws MalformedURLException { return new URL(baseUrl @@ -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/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(" ", "+"); + } } From 98d0ee1a8ec387927e24ac900a444396f1db9ae8 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 31 May 2023 13:47:29 +0200 Subject: [PATCH 07/37] #37 Added eBay Data Source + Docs --- CHANGELOG.md | 16 ++++ config/demo.config.properties | 4 +- src/main/java/de/rwu/easydrop/Main.java | 15 +++- .../api/client/EbayItemDataSource.java | 88 +++++++++++++++++++ .../de/rwu/easydrop/util/ConfigImplTest.java | 4 +- 5 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 010c0fd..bb12c81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.2.0-SNAPSHOT + +### Added + +- EbayItemDataSource (#37) + +### Changed + +- AbstractDataSource between DataSource interface and implementations to minimize code duplication + - Affects AmazonProductDataSource and related tests (~ #39) +- Testing resources moved to more intuitive location + +### Fixed + +- Logback configuration is now linked correctly + ## 0.1.0 ### Added 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/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index af1aeea..e776989 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -6,6 +6,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.rwu.easydrop.api.client.AmazonProductDataSource; +import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.util.Config; /** @@ -34,14 +35,22 @@ 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; + String ebayBaseUrl = config.getProperty("EBAY_API_URL"); + String ebayApiKey = config.getProperty("EBAY_API_KEY"); + + String amznTestProduct = null; + String ebayTestProduct = null; AmazonProductDataSource amznSrc = new AmazonProductDataSource(amznBaseUrl, amznApiKey); + EbayItemDataSource ebaySrc = new EbayItemDataSource(ebayBaseUrl, ebayApiKey); try { - testProduct = amznSrc.getProductDTOById("B096Y2TYKV").toString(); - LOGGER.info(testProduct); + amznTestProduct = amznSrc.getProductDTOById("B096Y2TYKV").toString(); + LOGGER.info(amznTestProduct); + ebayTestProduct = ebaySrc.getProductDTOById("Gigabyte GeForce RTX 3060").toString(); + LOGGER.info(ebayTestProduct); } catch (IllegalArgumentException e) { LOGGER.error("Something went wrong :(", e); } 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..ee998dc --- /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 + public 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 + */ + public 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/test/java/de/rwu/easydrop/util/ConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java index db7de79..0c5041e 100644 --- a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java +++ b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java @@ -13,7 +13,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"; @@ -74,7 +74,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) { From ecd368f8596c99df682a89e635f2d96c8a9fcae8 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Fri, 2 Jun 2023 19:11:15 +0200 Subject: [PATCH 08/37] Fixed singleton pattern in Config --- src/main/java/de/rwu/easydrop/util/Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/rwu/easydrop/util/Config.java b/src/main/java/de/rwu/easydrop/util/Config.java index 7a7f1e3..68be4a4 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; From f4314911602c28d7bf89d4c62ceec8edc8881c68 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Fri, 2 Jun 2023 19:11:30 +0200 Subject: [PATCH 09/37] Added config for local SQ instance --- .vscode/settings.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 +} From 17f0ba07d73f9660d615efeb2a6fc3172829e03c Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Fri, 2 Jun 2023 19:11:43 +0200 Subject: [PATCH 10/37] Added local SQ helper script --- Script/SonarQube_Local.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Script/SonarQube_Local.sh 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 From 5cd6cbacfe2e546940d92bc01751b47e794d550e Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Fri, 2 Jun 2023 19:11:52 +0200 Subject: [PATCH 11/37] Fix typo --- src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..510ca6d 100644 --- a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java +++ b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java @@ -118,7 +118,7 @@ public class ProductDTO { /** * 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) { From 059989cd7c9aab1e16a7f6a74026259995e9d87b Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Fri, 2 Jun 2023 19:12:33 +0200 Subject: [PATCH 12/37] Added tests for amazon data source --- .../api/client/AmazonProductDataSourceTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 aa319af..22c388a 100644 --- a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java @@ -188,4 +188,16 @@ class AmazonProductDataSourceTest { // 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); + } } From 7e998de280afb1c099b7d19cc203c5382cc4e5a3 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Fri, 2 Jun 2023 19:12:50 +0200 Subject: [PATCH 13/37] Adjusted Config tests for proper singleton pattern --- src/test/java/de/rwu/easydrop/util/ConfigTest.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 From ae7acef70995039949135216ed14c2ed69124b0a Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Fri, 2 Jun 2023 19:13:00 +0200 Subject: [PATCH 14/37] #37 Added tests for ebay data source --- .../api/client/EbayItemDataSourceTest.java | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java 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..27bcd78 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java @@ -0,0 +1,101 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +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 com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.ReadContext; + +import de.rwu.easydrop.api.dto.ProductDTO; + +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 { + String searchQuery = demoQuery; + URL apiUrl = demoDataSource.createApiUrl(searchQuery); + + 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); + } +} From 8d11d1537f2dd2e505c9df26d7897e70e0a4014f Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:07:01 +0200 Subject: [PATCH 15/37] Added Lombok and JUnit 5 Params dependencies --- pom.xml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6a81bd6..0537059 100644 --- a/pom.xml +++ b/pom.xml @@ -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 From 6902e72ab8c6823cf1d2c38e590a33cf790eba6e Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:07:23 +0200 Subject: [PATCH 16/37] Added Lombok config for proper JaCoCo coverage --- lombok.config | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 lombok.config 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 From 05487eb2852eea20441938f279eb4fb71fb82a71 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:08:10 +0200 Subject: [PATCH 17/37] Simplified via Lombok notation --- .../de/rwu/easydrop/api/dto/ProductDTO.java | 91 +------------------ 1 file changed, 2 insertions(+), 89 deletions(-) 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 510ca6d..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,120 +1,44 @@ 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. * @@ -125,15 +49,4 @@ public class ProductDTO { 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") + ")}"; - } } From d1bd6f6d9b7f5afb31b9f81864eb5bceb4265e63 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:08:33 +0200 Subject: [PATCH 18/37] Implemented Product --- .../de/rwu/easydrop/data/model/Product.java | 10 ---- .../rwu/easydrop/data/model/package-info.java | 6 --- .../java/de/rwu/easydrop/model/Product.java | 53 +++++++++++++++++++ .../de/rwu/easydrop/model/package-info.java | 6 +++ 4 files changed, 59 insertions(+), 16 deletions(-) delete mode 100644 src/main/java/de/rwu/easydrop/data/model/Product.java delete mode 100644 src/main/java/de/rwu/easydrop/data/model/package-info.java create mode 100644 src/main/java/de/rwu/easydrop/model/Product.java create mode 100644 src/main/java/de/rwu/easydrop/model/package-info.java 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/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; From 39176b5dc032aa8368d2fdb783596d6c3370bf51 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:10:31 +0200 Subject: [PATCH 19/37] Added/updated tests --- .../java/de/rwu/easydrop/util/Config.java | 7 ++ .../rwu/easydrop/api/dto/ProductDTOTest.java | 55 ++++++++++---- .../de/rwu/easydrop/model/ProductTest.java | 75 +++++++++++++++++++ .../de/rwu/easydrop/util/ConfigImplTest.java | 20 ++++- 4 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 src/test/java/de/rwu/easydrop/model/ProductTest.java diff --git a/src/main/java/de/rwu/easydrop/util/Config.java b/src/main/java/de/rwu/easydrop/util/Config.java index 68be4a4..32cce6a 100644 --- a/src/main/java/de/rwu/easydrop/util/Config.java +++ b/src/main/java/de/rwu/easydrop/util/Config.java @@ -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/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/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/util/ConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java index 0c5041e..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; @@ -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); }); @@ -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()); + } } From af8993cb0f8b32fd2fd6d5c710046f34c31832f0 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:11:50 +0200 Subject: [PATCH 20/37] #37 Added eBay data src, updated context + tests --- .../api/client/AbstractDataSource.java | 7 ++- .../api/client/AmazonProductDataSource.java | 4 +- .../api/client/DataSourceFactory.java | 55 +++++++++++++++++ .../api/client/EbayItemDataSource.java | 4 +- .../exception/DataSourceException.java | 27 ++++++++ .../exception/InvalidProductException.java | 27 ++++++++ .../rwu/easydrop/exception/package-info.java | 6 ++ .../client/AmazonProductDataSourceTest.java | 5 +- .../api/client/DataSourceFactoryTest.java | 48 +++++++++++++++ .../api/client/EbayItemDataSourceTest.java | 34 +++++++++-- .../exception/DataSourceExceptionTest.java | 61 +++++++++++++++++++ .../InvalidProductExceptionTest.java | 61 +++++++++++++++++++ 12 files changed, 325 insertions(+), 14 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java create mode 100644 src/main/java/de/rwu/easydrop/exception/DataSourceException.java create mode 100644 src/main/java/de/rwu/easydrop/exception/InvalidProductException.java create mode 100644 src/main/java/de/rwu/easydrop/exception/package-info.java create mode 100644 src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java create mode 100644 src/test/java/de/rwu/easydrop/exception/DataSourceExceptionTest.java create mode 100644 src/test/java/de/rwu/easydrop/exception/InvalidProductExceptionTest.java 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 9d1df1c..7751c10 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AbstractDataSource.java @@ -8,6 +8,7 @@ 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; /** @@ -38,7 +39,7 @@ public abstract class AbstractDataSource implements DataSource { * @param json Product data * @return Finished ProductDTO */ - public abstract ProductDTO buildProductDTO(ProductDTO product, String json); + protected abstract ProductDTO buildProductDTO(ProductDTO product, String json); /** * Overridable standard implementation. @@ -64,7 +65,7 @@ public abstract class AbstractDataSource implements DataSource { if (responseCode == HttpURLConnection.HTTP_OK) { reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); } else { - throw new IllegalArgumentException( + throw new DataSourceException( "Nothing found: " + dataOrigin + " API responded with error code " @@ -79,7 +80,7 @@ public abstract class AbstractDataSource implements DataSource { buildProductDTO(product, response.toString()); } catch (IOException e) { - throw new IllegalArgumentException( + throw new DataSourceException( "Couldn't fulfill " + dataOrigin + " API request"); 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 5654765..83b758d 100644 --- a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java @@ -48,7 +48,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { } @Override - 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); @@ -71,7 +71,7 @@ public final class AmazonProductDataSource extends AbstractDataSource { * @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 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 index ee998dc..171b8b9 100644 --- a/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java +++ b/src/main/java/de/rwu/easydrop/api/client/EbayItemDataSource.java @@ -43,7 +43,7 @@ public final class EbayItemDataSource extends AbstractDataSource { * @param searchQuery Exact product name or other valid identifier. */ @Override - public URL createApiUrl(final String searchQuery) throws MalformedURLException { + protected URL createApiUrl(final String searchQuery) throws MalformedURLException { return new URL(baseUrl + "/buy/browse/v1/item_summary/search?q=" + searchQuery @@ -57,7 +57,7 @@ public final class EbayItemDataSource extends AbstractDataSource { * @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 = "$.itemSummaries[0]."; ReadContext ctx = JsonPath.parse(json); 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/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java index 22c388a..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 { @@ -158,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); }); @@ -181,7 +182,7 @@ 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); }); 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 index 27bcd78..9a6ed45 100644 --- a/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/EbayItemDataSourceTest.java @@ -2,8 +2,13 @@ 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; @@ -12,10 +17,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.MockitoAnnotations; -import com.jayway.jsonpath.JsonPath; -import com.jayway.jsonpath.ReadContext; - import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.DataSourceException; class EbayItemDataSourceTest { private EbayItemDataSource demoDataSource; @@ -51,8 +54,7 @@ class EbayItemDataSourceTest { @Test void createApiUrl_ValidSearchQuery_ReturnsValidUrl() throws MalformedURLException { - String searchQuery = demoQuery; - URL apiUrl = demoDataSource.createApiUrl(searchQuery); + 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", @@ -98,4 +100,26 @@ class EbayItemDataSourceTest { 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/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"); + }); + } +} From 7f579554004e3a171de121e059b691beebcfa262 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:12:35 +0200 Subject: [PATCH 21/37] Added Product mapping and validation --- .../service/mapping/ProductMapper.java | 34 +++++- .../service/mapping/package-info.java | 2 +- .../service/validation/ProductValidator.java | 50 +++++++- .../service/validation/package-info.java | 2 +- .../service/mapping/ProductMapperTest.java | 56 +++++++++ .../validation/ProductValidatorTest.java | 109 ++++++++++++++++++ 6 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/validation/ProductValidatorTest.java 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/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/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/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; + } +} From 33dcfaafa5ccc79bbd9ea0eadd3f338f19a5b85a Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:12:57 +0200 Subject: [PATCH 22/37] Added product retriever with tests --- .../service/retriever/ProductRetriever.java | 67 ++++++++++++++ .../service/retriever/package-info.java | 6 ++ .../retriever/ProductRetrieverTest.java | 87 +++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java create mode 100644 src/main/java/de/rwu/easydrop/service/retriever/package-info.java create mode 100644 src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java 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/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); + } +} From c5bfdd83ead41dbeca4cfff9b4d231cbd7cd578f Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:13:12 +0200 Subject: [PATCH 23/37] Updated test resource config --- src/test/resources/testdata.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/resources/testdata.properties b/src/test/resources/testdata.properties index 854257f..71176b3 100644 --- a/src/test/resources/testdata.properties +++ b/src/test/resources/testdata.properties @@ -1 +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 From cccb466bd2b193b46fab84282c7c66d49d7d5df1 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:13:43 +0200 Subject: [PATCH 24/37] Updated Main demo method --- src/main/java/de/rwu/easydrop/Main.java | 29 +++++++------------------ 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index e776989..74b6aec 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -5,8 +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.EbayItemDataSource; +import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.service.retriever.ProductRetriever; import de.rwu.easydrop.util.Config; /** @@ -34,25 +34,12 @@ public final class Main { */ public static void main(final String[] args) throws ConfigurationException { Config config = Config.getInstance(); - config.loadConfig(); + DataSourceFactory dataSourceFactory = new DataSourceFactory(config); + ProductRetriever retriever = new ProductRetriever(dataSourceFactory); - String amznBaseUrl = config.getProperty("AMAZON_API_URL"); - String amznApiKey = config.getProperty("AMAZON_API_KEY"); - String ebayBaseUrl = config.getProperty("EBAY_API_URL"); - String ebayApiKey = config.getProperty("EBAY_API_KEY"); - - String amznTestProduct = null; - String ebayTestProduct = null; - - AmazonProductDataSource amznSrc = new AmazonProductDataSource(amznBaseUrl, amznApiKey); - EbayItemDataSource ebaySrc = new EbayItemDataSource(ebayBaseUrl, ebayApiKey); - try { - amznTestProduct = amznSrc.getProductDTOById("B096Y2TYKV").toString(); - LOGGER.info(amznTestProduct); - ebayTestProduct = ebaySrc.getProductDTOById("Gigabyte GeForce RTX 3060").toString(); - LOGGER.info(ebayTestProduct); - } 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); } } From 431ea685bdf791c996143f3d8d20cc7329b7809e Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:14:11 +0200 Subject: [PATCH 25/37] #37 Added changes to changelog --- CHANGELOG.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb12c81..4daf9c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,23 +4,31 @@ ### Added -- EbayItemDataSource (#37) +- `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 +- `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) From 4f8008c6b005127f5c5adadb385d508b996db209 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:33:03 +0200 Subject: [PATCH 26/37] #66 Added PITest dependencies and plugin --- pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pom.xml b/pom.xml index 0537059..d45d187 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,12 @@ 2.8.0 + + org.pitest + pitest-junit5-plugin + 1.2.0 + + org.slf4j slf4j-api @@ -101,6 +107,12 @@ jacoco-maven-plugin 0.8.10 + + + org.pitest + pitest-maven + 1.14.1 + From 126613ef3c770c996cc97ec330413a73d810af2f Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:33:16 +0200 Subject: [PATCH 27/37] #66 Added script for ease of use --- Script/MutationTestReport.sh | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Script/MutationTestReport.sh diff --git a/Script/MutationTestReport.sh b/Script/MutationTestReport.sh new file mode 100644 index 0000000..beadbec --- /dev/null +++ b/Script/MutationTestReport.sh @@ -0,0 +1,2 @@ +mvn test-compile org.pitest:pitest-maven:mutationCoverage +start target/pit-reports/index.html \ No newline at end of file From 34c682b314eabbf08b67bf11f6ebc1b390a5ca8b Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Tue, 6 Jun 2023 02:38:23 +0200 Subject: [PATCH 28/37] #66 Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4daf9c2..d62761a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,13 @@ ### Added - `EbayItemDataSource` (#37) +- Mutation testing capabilities via PITest (#66) - `Product` class - Mapping between Product and its corresponding DTO - Dependencies: Lombok for easier notation, JUnit 5 Params for repetitive tests - Product Validation - Product Retrieval -- Tests +- Unit Tests ### Changed From f50a1e998de669d874dcdaf7dabd18245d9627f5 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 7 Jun 2023 05:45:10 +0200 Subject: [PATCH 29/37] Added auto-opener to SQ utility script --- Script/SonarQube_Local.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Script/SonarQube_Local.sh b/Script/SonarQube_Local.sh index 18b0b37..14d2602 100644 --- a/Script/SonarQube_Local.sh +++ b/Script/SonarQube_Local.sh @@ -2,4 +2,6 @@ mvn clean verify sonar:sonar -Pcoverage \ -Dsonar.projectKey=EasyDrop \ -Dsonar.projectName='EasyDrop' \ -Dsonar.host.url=http://localhost:9000 \ - -Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931 \ No newline at end of file + -Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931 + +start http://localhost:9000/dashboard?id=EasyDrop \ No newline at end of file From b9fb6cbc89c4351bab55b3285630b44e96947c95 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 7 Jun 2023 05:45:28 +0200 Subject: [PATCH 30/37] Added demo products config --- config/demo.products-config.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/demo.products-config.json diff --git a/config/demo.products-config.json b/config/demo.products-config.json new file mode 100644 index 0000000..2aa9058 --- /dev/null +++ b/config/demo.products-config.json @@ -0,0 +1,16 @@ +{ + "products": [ + { + "name": "Gigabyte GeForce RTX 3060", + "description": "Very epic GPU", + "identifiers": [ + { + "Amazon": "B096Y2TYKV" + }, + { + "eBay": "Gigabyte GeForce RTX 3060" + } + ] + } + ] +} From 3ba366684c478e294abd6c130c3ab1f939e81e65 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 7 Jun 2023 05:46:03 +0200 Subject: [PATCH 31/37] Added utility structures + tests --- .gitignore | 1 + src/main/java/de/rwu/easydrop/Main.java | 14 +- .../rwu/easydrop/model/ProductCatalogue.java | 79 ++++++++++ .../de/rwu/easydrop/service/package-info.java | 2 +- .../service/processing/package-info.java | 2 +- .../service/retriever/CatalogueRetriever.java | 75 +++++++++ .../java/de/rwu/easydrop/util/Config.java | 1 - .../de/rwu/easydrop/util/ProductsConfig.java | 142 ++++++++++++++++++ .../de/rwu/easydrop/util/package-info.java | 2 +- .../easydrop/model/ProductCatalogueTest.java | 93 ++++++++++++ .../retriever/CatalogueRetrieverTest.java | 75 +++++++++ .../de/rwu/easydrop/util/ConfigImplTest.java | 6 +- .../easydrop/util/ProductsConfigImplTest.java | 68 +++++++++ .../rwu/easydrop/util/ProductsConfigTest.java | 44 ++++++ ...data.properties => test.config.properties} | 0 .../resources/test.empty.products-config.json | 0 ...empty.properties => test.empty.properties} | 0 .../test.malformed.products-config.json | 15 ++ src/test/resources/test.products-config.json | 16 ++ 19 files changed, 624 insertions(+), 11 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/model/ProductCatalogue.java create mode 100644 src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java create mode 100644 src/main/java/de/rwu/easydrop/util/ProductsConfig.java create mode 100644 src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java create mode 100644 src/test/java/de/rwu/easydrop/util/ProductsConfigImplTest.java create mode 100644 src/test/java/de/rwu/easydrop/util/ProductsConfigTest.java rename src/test/resources/{testdata.properties => test.config.properties} (100%) create mode 100644 src/test/resources/test.empty.products-config.json rename src/test/resources/{empty.properties => test.empty.properties} (100%) create mode 100644 src/test/resources/test.malformed.products-config.json create mode 100644 src/test/resources/test.products-config.json diff --git a/.gitignore b/.gitignore index 24affe0..ae3d76c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ ################################################################################################### config.properties +products-config.json ################################################################################################### ## Visual Studio Code ############################################################################# diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index 74b6aec..2a0ff06 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -6,8 +6,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.rwu.easydrop.api.client.DataSourceFactory; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.retriever.CatalogueRetriever; import de.rwu.easydrop.service.retriever.ProductRetriever; import de.rwu.easydrop.util.Config; +import de.rwu.easydrop.util.ProductsConfig; /** * Kickoff point for the service. @@ -34,12 +37,15 @@ public final class Main { */ public static void main(final String[] args) throws ConfigurationException { Config config = Config.getInstance(); + ProductsConfig pConfig = ProductsConfig.getInstance(); DataSourceFactory dataSourceFactory = new DataSourceFactory(config); ProductRetriever retriever = new ProductRetriever(dataSourceFactory); + CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever); - String amznProduct = retriever.getProductFromAmazon("B096Y2TYKV").toString(); - LOGGER.info(amznProduct); - String ebayProduct = retriever.getProductFromEbay("Gigabyte GeForce RTX 3060").toString(); - LOGGER.info(ebayProduct); + catRetriever.loadCatalogues(); + for (ProductCatalogue pCat : catRetriever.getProductCatalogues()) { + String pCatStr = pCat.toString(); + LOGGER.info(pCatStr); + } } } diff --git a/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java new file mode 100644 index 0000000..97f31dc --- /dev/null +++ b/src/main/java/de/rwu/easydrop/model/ProductCatalogue.java @@ -0,0 +1,79 @@ +package de.rwu.easydrop.model; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Data; + +/** + * Holds Product instances for one product from different data sources. + * + * @since 0.2.0 + */ +@Data +public class ProductCatalogue { + /** + * Product name. + */ + private String productName; + /** + * Product description. + */ + private String description; + /** + * Product collection. + */ + private List products; + + /** + * Creates new Product Catalogue. + * + * @param newProductName + * @param newDescription + */ + public ProductCatalogue(final String newProductName, final String newDescription) { + this.productName = newProductName; + this.description = newDescription; + this.products = new ArrayList<>(); + } + + /** + * Adds a product to the catalogue. + * + * @param product + */ + public void addProduct(final Product product) { + products.add(product); + } + + /** + * Removes a product from the catalogue. + * + * @param product + */ + public void removeProduct(final Product product) { + products.remove(product); + } + + /** + * Removes all products from the catalogue. + */ + public void clearProducts() { + products = new ArrayList<>(); + } + + /** + * Outputs the catalogue as a string. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Product Catalogue: ").append(productName).append("\n"); + sb.append("Description: ").append(description).append("\n"); + sb.append("Products:\n"); + for (Product product : products) { + sb.append(product.toString()).append("\n"); + } + return sb.toString(); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/package-info.java b/src/main/java/de/rwu/easydrop/service/package-info.java index ff59e0c..18ad0d7 100644 --- a/src/main/java/de/rwu/easydrop/service/package-info.java +++ b/src/main/java/de/rwu/easydrop/service/package-info.java @@ -1,6 +1,6 @@ /** * Packages for supporting business logic. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.service; diff --git a/src/main/java/de/rwu/easydrop/service/processing/package-info.java b/src/main/java/de/rwu/easydrop/service/processing/package-info.java index db7cb9e..c373207 100644 --- a/src/main/java/de/rwu/easydrop/service/processing/package-info.java +++ b/src/main/java/de/rwu/easydrop/service/processing/package-info.java @@ -1,6 +1,6 @@ /** * Supports diverse business processes and enforces business rules. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.service.processing; diff --git a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java new file mode 100644 index 0000000..e800938 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java @@ -0,0 +1,75 @@ +package de.rwu.easydrop.service.retriever; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.ConfigurationException; + +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.util.ProductsConfig; +import lombok.Data; + +/** + * Retrieves data for all products of multiple catalogues. + * + * @since 0.2.0 + */ +@Data +public class CatalogueRetriever { + /** + * User-configured products. + */ + private ProductsConfig productsConfig; + /** + * Product Retriever. + */ + private ProductRetriever productRetriever; + /** + * Product catalogue. + */ + private List productCatalogues; + + /** + * Creates a new instance. + * + * @param newProductsConfig + * @param newProductRetriever + */ + public CatalogueRetriever( + final ProductsConfig newProductsConfig, final ProductRetriever newProductRetriever) { + productRetriever = newProductRetriever; + productsConfig = newProductsConfig; + productCatalogues = new ArrayList<>(); + } + + /** + * Loads catalogues as configured by the user. + * + * @throws ConfigurationException + */ + public void loadCatalogues() throws ConfigurationException { + productsConfig.loadConfig(); + + for (ProductCatalogue pCat : productsConfig.getProductCatalogues()) { + ProductCatalogue newProductCatalogue = new ProductCatalogue( + pCat.getProductName(), pCat.getDescription()); + + for (Product product : pCat.getProducts()) { + Product newProduct = new Product(); + newProduct.setDataOrigin(product.getDataOrigin()); + newProduct.setProductId(product.getProductId()); + + if (product.getDataOrigin().equals("Amazon")) { + newProduct = productRetriever.getProductFromAmazon(product.getProductId()); + } else if (product.getDataOrigin().equals("eBay")) { + newProduct = productRetriever.getProductFromEbay(product.getProductId()); + } + + newProductCatalogue.addProduct(newProduct); + } + + productCatalogues.add(newProductCatalogue); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/util/Config.java b/src/main/java/de/rwu/easydrop/util/Config.java index 32cce6a..e40b3d4 100644 --- a/src/main/java/de/rwu/easydrop/util/Config.java +++ b/src/main/java/de/rwu/easydrop/util/Config.java @@ -52,7 +52,6 @@ public final class Config { * Returns current config instance. * * @return Config instance - * @throws ConfigurationException */ public static Config getInstance() { if (instance == null) { diff --git a/src/main/java/de/rwu/easydrop/util/ProductsConfig.java b/src/main/java/de/rwu/easydrop/util/ProductsConfig.java new file mode 100644 index 0000000..b69777f --- /dev/null +++ b/src/main/java/de/rwu/easydrop/util/ProductsConfig.java @@ -0,0 +1,142 @@ +package de.rwu.easydrop.util; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import javax.naming.ConfigurationException; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.JsonPathException; + +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; + +/** + * Reads the user-specified catalogue of products. + * + * @since 0.2.0 + */ +public final class ProductsConfig { + /** + * Products config location. + */ + private String configLocation = "config/products-config.json"; + + /** + * @return the configLocation + */ + public String getConfigLocation() { + return configLocation; + } + + /** + * @param newConfigLocation the configLocation to set + */ + public void setConfigLocation(final String newConfigLocation) { + this.configLocation = newConfigLocation; + } + + /** + * Holds the configured products. + */ + private List productCatalogues; + + /** + * @return the product catalogues + */ + public List getProductCatalogues() { + return productCatalogues; + } + + /** + * Singleton instance. + */ + private static ProductsConfig instance = null; + + /** + * Private constructor to prevent external instantiation. + */ + private ProductsConfig() { + productCatalogues = new ArrayList<>(); + } + + /** + * Returns current products config instance. + * + * @return Products Config instance + */ + public static ProductsConfig getInstance() { + if (instance == null) { + instance = new ProductsConfig(); + } + + return instance; + } + + /** + * Loads user-specified configuration into productCatalogues attribute. + * + * @throws ConfigurationException + */ + public void loadConfig() throws ConfigurationException { + try { + File jsonFile = new File(configLocation).getAbsoluteFile(); + ArrayList> jsonProducts = JsonPath.read(jsonFile, "$.products"); + + setProductCatalogues(jsonProducts); + } catch (IOException e) { + throw new ConfigurationException("Couldn't load required products config file"); + } catch (JsonPathException e) { + throw new ConfigurationException("Products config is empty or malformed"); + } + } + + /** + * Loads catalogues from JSON. + * + * @param jsonCatalogues + */ + private void setProductCatalogues(final ArrayList> jsonCatalogues) { + for (HashMap productCatalogue : jsonCatalogues) { + String name = productCatalogue.get("name").toString(); + String desc = productCatalogue.get("description").toString(); + String identifiers = productCatalogue.get("identifiers").toString(); + + ProductCatalogue pCat = new ProductCatalogue(name, desc); + + addProductsToCatalogue(identifiers, pCat); + productCatalogues.add(pCat); + } + } + + /** + * Loads products from JSON. + * + * @param jsonIdentifiers + * @param pCat + */ + private void addProductsToCatalogue(final String jsonIdentifiers, final ProductCatalogue pCat) { + ArrayList> identifiers = JsonPath.read(jsonIdentifiers, "$"); + + for (HashMap product : identifiers) { + String dataOrigin = product.keySet().iterator().next(); + String identifier = product.get(dataOrigin).toString(); + + Product newProduct = new Product(); + newProduct.setDataOrigin(dataOrigin); + newProduct.setProductId(identifier); + + pCat.addProduct(newProduct); + } + } + + /** + * Resets the contained product catalogues. + */ + public void reset() { + productCatalogues = new ArrayList<>(); + } +} diff --git a/src/main/java/de/rwu/easydrop/util/package-info.java b/src/main/java/de/rwu/easydrop/util/package-info.java index 40789f2..ed8e5eb 100644 --- a/src/main/java/de/rwu/easydrop/util/package-info.java +++ b/src/main/java/de/rwu/easydrop/util/package-info.java @@ -1,6 +1,6 @@ /** * General utility such as formatting helpers. * - * TODO implement + * @since 0.1.0 */ package de.rwu.easydrop.util; diff --git a/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java new file mode 100644 index 0000000..f070e2d --- /dev/null +++ b/src/test/java/de/rwu/easydrop/model/ProductCatalogueTest.java @@ -0,0 +1,93 @@ +package de.rwu.easydrop.model; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ProductCatalogueTest { + private ProductCatalogue productCatalogue; + + @BeforeEach + public void setup() { + productCatalogue = new ProductCatalogue("GPU", "Graphics Processing Units"); + } + + @Test + void testAddProduct() { + Product product = new Product(); + product.setProductId("12345"); + product.setMerchant("AmazonSeller"); + product.setDataOrigin("Amazon"); + productCatalogue.addProduct(product); + + List products = productCatalogue.getProducts(); + Assertions.assertEquals(1, products.size()); + Assertions.assertEquals(product, products.get(0)); + } + + @Test + void testRemoveProduct() { + Product product1 = new Product(); + product1.setProductId("12345"); + product1.setMerchant("AmazonSeller"); + product1.setDataOrigin("Amazon"); + productCatalogue.addProduct(product1); + + Product product2 = new Product(); + product2.setProductId("54321"); + product2.setMerchant("eBaySeller"); + product2.setDataOrigin("eBay"); + productCatalogue.addProduct(product2); + + productCatalogue.removeProduct(product1); + + List products = productCatalogue.getProducts(); + Assertions.assertEquals(1, products.size()); + Assertions.assertEquals(product2, products.get(0)); + } + + @Test + void testClearProducts() { + Product product1 = new Product(); + product1.setProductId("12345"); + product1.setMerchant("AmazonSeller"); + product1.setDataOrigin("Amazon"); + productCatalogue.addProduct(product1); + + Product product2 = new Product(); + product2.setProductId("54321"); + product2.setMerchant("eBay"); + product2.setDataOrigin("eBay"); + productCatalogue.addProduct(product2); + + productCatalogue.clearProducts(); + + List products = productCatalogue.getProducts(); + Assertions.assertTrue(products.isEmpty()); + } + + @Test + void testToString() { + Product product1 = new Product(); + product1.setProductId("12345"); + product1.setMerchant("AmazonSeller"); + product1.setDataOrigin("Amazon"); + productCatalogue.addProduct(product1); + + Product product2 = new Product(); + product2.setProductId("54321"); + product2.setMerchant("eBaySeller"); + product2.setDataOrigin("eBay"); + productCatalogue.addProduct(product2); + + String expectedString = "Product Catalogue: GPU\n" + + "Description: Graphics Processing Units\n" + + "Products:\n" + + "Product: [12345 from AmazonSeller (Amazon) at 0,00 € (available: no)]\n" + + "Product: [54321 from eBaySeller (eBay) at 0,00 € (available: no)]\n"; + + Assertions.assertEquals(expectedString, productCatalogue.toString()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java new file mode 100644 index 0000000..a0721e9 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java @@ -0,0 +1,75 @@ +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.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.util.ProductsConfig; + +class CatalogueRetrieverTest { + private ProductsConfig productsConfig; + private ProductRetriever productRetriever; + private CatalogueRetriever catalogueRetriever; + + @BeforeEach + public void setup() { + productsConfig = mock(ProductsConfig.class); + productRetriever = mock(ProductRetriever.class); + catalogueRetriever = new CatalogueRetriever(productsConfig, productRetriever); + } + + @Test + void loadCatalogues_ValidConfig_CataloguesLoaded() throws ConfigurationException { + // Arrange + List productCatalogues = new ArrayList<>(); + + // Create a sample product catalogue with two products + ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue"); + Product product1 = new Product(); + product1.setDataOrigin("Amazon"); + product1.setProductId("ASIN1"); + productCatalogue.addProduct(product1); + Product product2 = new Product(); + product2.setDataOrigin("eBay"); + product2.setProductId("ProductID2"); + productCatalogue.addProduct(product2); + + productCatalogues.add(productCatalogue); + + // Mock the methods + when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues); + when(productRetriever.getProductFromAmazon("ASIN1")).thenReturn(product1); + when(productRetriever.getProductFromEbay("ProductID2")).thenReturn(product2); + + // Act + catalogueRetriever.loadCatalogues(); + + // Assert + List loadedCatalogues = catalogueRetriever.getProductCatalogues(); + assertEquals(1, loadedCatalogues.size()); + + ProductCatalogue loadedCatalogue = loadedCatalogues.get(0); + assertEquals("Catalogue 1", loadedCatalogue.getProductName()); + assertEquals("Sample catalogue", loadedCatalogue.getDescription()); + List loadedProducts = loadedCatalogue.getProducts(); + assertEquals(2, loadedProducts.size()); + assertEquals(product1, loadedProducts.get(0)); + assertEquals(product2, loadedProducts.get(1)); + + // Verify the method invocations + verify(productsConfig).loadConfig(); + verify(productRetriever).getProductFromAmazon("ASIN1"); + verify(productRetriever).getProductFromEbay("ProductID2"); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java index 59cece7..2205e73 100644 --- a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java +++ b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java @@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test; class ConfigImplTest { private Config config; - private final static String TESTDATA_PATH = "src/test/resources/testdata.properties"; + private final static String TESTDATA_PATH = "src/test/resources/test.config.properties"; private final static String TESTDATA_KEY = "API_KEY"; private final static String TESTDATA_VAL = "keyIsHere"; @@ -77,7 +77,7 @@ class ConfigImplTest { @Test void testLoadConfigSuccessfully() { try { - config.setConfigLocation("src/test/resources/testdata.properties"); + config.setConfigLocation("src/test/resources/test.config.properties"); config.loadConfig(); assertEquals(TESTDATA_VAL, config.getProperty(TESTDATA_KEY)); } catch (ConfigurationException e) { @@ -98,7 +98,7 @@ class ConfigImplTest { @Test void testReset() throws ConfigurationException { - config.setConfigLocation("src/test/resources/testdata.properties"); + config.setConfigLocation("src/test/resources/test.config.properties"); config.loadConfig(); assertNotNull(config.getProperty(TESTDATA_KEY)); diff --git a/src/test/java/de/rwu/easydrop/util/ProductsConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ProductsConfigImplTest.java new file mode 100644 index 0000000..cec62ad --- /dev/null +++ b/src/test/java/de/rwu/easydrop/util/ProductsConfigImplTest.java @@ -0,0 +1,68 @@ +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.fail; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ProductsConfigImplTest { + private ProductsConfig config; + private final static String TESTDATA_PATH = "src/test/resources/test.products-config.json"; + private final static String TESTDATA_EMPTY_PATH = "src/test/resources/test.empty.products-config.json"; + private final static String TESTDATA_MALFORMED_PATH = "src/test/resources/test.malformed.products-config.json"; + + @BeforeEach + void setUp() { + config = ProductsConfig.getInstance(); + } + + @Test + void testLoadConfigSuccessfully() { + try { + config.setConfigLocation(TESTDATA_PATH); + config.loadConfig(); + String value = config.getProductCatalogues().get(0).getProductName(); + + assertEquals("Demo Product", value); + } catch (ConfigurationException e) { + fail("ConfigurationException should not be thrown"); + } + } + + @Test + void testLoadConfigMissingFile() { + config.setConfigLocation("path/that/doesnt/exist/products-config.json"); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + config.loadConfig(); + }); + + assertEquals("Couldn't load required products config file", exception.getMessage()); + } + + @Test + void testLoadConfigEmptyFile() { + config.setConfigLocation(TESTDATA_EMPTY_PATH); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + config.loadConfig(); + }); + + assertEquals("Products config is empty or malformed", exception.getMessage()); + } + + @Test + void testLoadConfigMalformedFile() { + config.setConfigLocation(TESTDATA_MALFORMED_PATH); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + config.loadConfig(); + }); + + assertEquals("Products config is empty or malformed", exception.getMessage()); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/ProductsConfigTest.java b/src/test/java/de/rwu/easydrop/util/ProductsConfigTest.java new file mode 100644 index 0000000..b791d2a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/util/ProductsConfigTest.java @@ -0,0 +1,44 @@ +package de.rwu.easydrop.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.spy; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +class ProductsConfigTest { + private ProductsConfig productsConfig; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + productsConfig = spy(ProductsConfig.getInstance()); + } + + @Test + void testGetInstance() { + ProductsConfig newConfig = ProductsConfig.getInstance(); + assertNotNull(newConfig); + } + + @Test + void testGetInstanceEquality() { + // Create "two" instances to check validity of Singleton pattern + ProductsConfig instance1 = ProductsConfig.getInstance(); + ProductsConfig instance2 = ProductsConfig.getInstance(); + + assertSame(instance1, instance2, "Instances should be equal"); + } + + @Test + void testSetConfigLocation() { + String newPath = "new/location/config.properties"; + productsConfig.setConfigLocation(newPath); + assertEquals(newPath, productsConfig.getConfigLocation()); + } +} diff --git a/src/test/resources/testdata.properties b/src/test/resources/test.config.properties similarity index 100% rename from src/test/resources/testdata.properties rename to src/test/resources/test.config.properties diff --git a/src/test/resources/test.empty.products-config.json b/src/test/resources/test.empty.products-config.json new file mode 100644 index 0000000..e69de29 diff --git a/src/test/resources/empty.properties b/src/test/resources/test.empty.properties similarity index 100% rename from src/test/resources/empty.properties rename to src/test/resources/test.empty.properties diff --git a/src/test/resources/test.malformed.products-config.json b/src/test/resources/test.malformed.products-config.json new file mode 100644 index 0000000..00d0092 --- /dev/null +++ b/src/test/resources/test.malformed.products-config.json @@ -0,0 +1,15 @@ +{ + "products": [ + { + "name": "Demo Product", + description: "Integration Testing Product", + "identifiers": [ + { + "Amazon": "DEMO-AMAZON-001" + }, + { + "eBay": "DEMO-EBAY-001" + } + ] + ] +} diff --git a/src/test/resources/test.products-config.json b/src/test/resources/test.products-config.json new file mode 100644 index 0000000..44262c8 --- /dev/null +++ b/src/test/resources/test.products-config.json @@ -0,0 +1,16 @@ +{ + "products": [ + { + "name": "Demo Product", + "description": "Integration Testing Product", + "identifiers": [ + { + "Amazon": "DEMO-AMAZON-001" + }, + { + "eBay": "DEMO-EBAY-001" + } + ] + } + ] +} From 0a42a38016b79c247d4c61056d7a6b9f1345bcb0 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 7 Jun 2023 06:41:29 +0200 Subject: [PATCH 32/37] Adjusted test file names --- .../util/{ConfigImplTest.java => ConfigIntegrationTest.java} | 2 +- ...tsConfigImplTest.java => ProductsConfigIntegrationTest.java} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/test/java/de/rwu/easydrop/util/{ConfigImplTest.java => ConfigIntegrationTest.java} (99%) rename src/test/java/de/rwu/easydrop/util/{ProductsConfigImplTest.java => ProductsConfigIntegrationTest.java} (98%) diff --git a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ConfigIntegrationTest.java similarity index 99% rename from src/test/java/de/rwu/easydrop/util/ConfigImplTest.java rename to src/test/java/de/rwu/easydrop/util/ConfigIntegrationTest.java index 2205e73..2df710d 100644 --- a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java +++ b/src/test/java/de/rwu/easydrop/util/ConfigIntegrationTest.java @@ -12,7 +12,7 @@ import javax.naming.ConfigurationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class ConfigImplTest { +class ConfigIntegrationTest { private Config config; private final static String TESTDATA_PATH = "src/test/resources/test.config.properties"; private final static String TESTDATA_KEY = "API_KEY"; diff --git a/src/test/java/de/rwu/easydrop/util/ProductsConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java similarity index 98% rename from src/test/java/de/rwu/easydrop/util/ProductsConfigImplTest.java rename to src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java index cec62ad..b848b17 100644 --- a/src/test/java/de/rwu/easydrop/util/ProductsConfigImplTest.java +++ b/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java @@ -9,7 +9,7 @@ import javax.naming.ConfigurationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class ProductsConfigImplTest { +class ProductsConfigIntegrationTest { private ProductsConfig config; private final static String TESTDATA_PATH = "src/test/resources/test.products-config.json"; private final static String TESTDATA_EMPTY_PATH = "src/test/resources/test.empty.products-config.json"; From 5a6ff718396145770a9366ae63c0b05593a136f8 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 7 Jun 2023 23:17:59 +0200 Subject: [PATCH 33/37] #53 Implemented SQLite product data persistence + tests --- .gitignore | 1 + pom.xml | 6 + src/main/java/de/rwu/easydrop/Main.java | 13 +- .../api/client/DataSourceFactory.java | 26 +++ .../connector/AbstractProductPersistence.java | 34 +++ .../data/connector/DatabaseConnector.java | 10 - .../data/connector/SQLiteConnector.java | 173 ++++++++++++++++ .../easydrop/data/connector/package-info.java | 2 +- .../de/rwu/easydrop/data/dao/ProductDAO.java | 10 - .../rwu/easydrop/data/dao/package-info.java | 6 - .../de/rwu/easydrop/data/package-info.java | 2 +- .../exception/PersistenceException.java | 27 +++ .../service/mapping/ProductMapper.java | 22 +- .../service/retriever/CatalogueRetriever.java | 7 +- .../service/retriever/ProductRetriever.java | 17 ++ .../service/writer/CatalogueWriter.java | 47 +++++ .../service/writer/ProductWriter.java | 38 ++++ .../easydrop/service/writer/package-info.java | 6 + .../api/client/DataSourceFactoryTest.java | 21 ++ .../data/connector/SQLiteConnectorTest.java | 195 ++++++++++++++++++ .../exception/PersistenceExceptionTest.java | 28 +++ .../service/mapping/ProductMapperTest.java | 24 +++ .../retriever/CatalogueRetrieverTest.java | 30 ++- .../retriever/ProductRetrieverTest.java | 24 +++ .../service/writer/CatalogueWriterTest.java | 69 +++++++ .../service/writer/ProductWriterTest.java | 55 +++++ .../util/ProductsConfigIntegrationTest.java | 21 ++ 27 files changed, 880 insertions(+), 34 deletions(-) create mode 100644 src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java delete mode 100644 src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java create mode 100644 src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java delete mode 100644 src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java delete mode 100644 src/main/java/de/rwu/easydrop/data/dao/package-info.java create mode 100644 src/main/java/de/rwu/easydrop/exception/PersistenceException.java create mode 100644 src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java create mode 100644 src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java create mode 100644 src/main/java/de/rwu/easydrop/service/writer/package-info.java create mode 100644 src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java create mode 100644 src/test/java/de/rwu/easydrop/exception/PersistenceExceptionTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java create mode 100644 src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java diff --git a/.gitignore b/.gitignore index ae3d76c..7d81f52 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ config.properties products-config.json +persistence.db ################################################################################################### ## Visual Studio Code ############################################################################# diff --git a/pom.xml b/pom.xml index d45d187..58603a6 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,12 @@ logback-classic 1.4.7 + + + org.xerial + sqlite-jdbc + 3.42.0.0 + diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java index 2a0ff06..5441d96 100644 --- a/src/main/java/de/rwu/easydrop/Main.java +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -1,14 +1,20 @@ 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; @@ -41,9 +47,14 @@ public final class Main { 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(); - for (ProductCatalogue pCat : catRetriever.getProductCatalogues()) { + List pCats = catRetriever.getProductCatalogues(); + catWriter.writeCatalogues(pCats); + + for (ProductCatalogue pCat : pCats) { String pCatStr = pCat.toString(); LOGGER.info(pCatStr); } diff --git a/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java index ba79ab5..490466c 100644 --- a/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java +++ b/src/main/java/de/rwu/easydrop/api/client/DataSourceFactory.java @@ -2,6 +2,8 @@ package de.rwu.easydrop.api.client; import javax.naming.ConfigurationException; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.util.Config; /** @@ -15,6 +17,17 @@ public class DataSourceFactory { * The data source config. */ private Config config; + /** + * Persistence interface. + */ + private AbstractProductPersistence persistence = null; + + /** + * @param newPersistence the persistence to set + */ + public void setPersistence(final AbstractProductPersistence newPersistence) { + this.persistence = newPersistence; + } /** * @param newConfig the config to set @@ -52,4 +65,17 @@ public class DataSourceFactory { String apiKey = config.getProperty("EBAY_API_KEY"); return new EbayItemDataSource(apiUrl, apiKey); } + + /** + * Creates a persistence data source. + * + * @return ProductPersistenceInterface + */ + public AbstractProductPersistence createProductPersistenceDataSource() { + if (persistence == null) { + throw new PersistenceException("Persistence is not set"); + } + + return persistence; + } } diff --git a/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java b/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java new file mode 100644 index 0000000..6a302dc --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/AbstractProductPersistence.java @@ -0,0 +1,34 @@ +package de.rwu.easydrop.data.connector; + +import de.rwu.easydrop.api.client.AbstractDataSource; +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Allows connecting to a persistent product data store. + * + * @since 0.2.0 + */ +public abstract class AbstractProductPersistence extends AbstractDataSource { + /** + * Data origin. + */ + public static final String DATA_ORIGIN = "Persistence"; + + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + public abstract void saveProduct(ProductDTO dto); + + /** + * Gets a ProductDTO from persistence. + */ + @Override + public abstract ProductDTO getProductDTOById(String productId); + + /** + * Deletes all data from persistence. + */ + public abstract void clearData(); +} diff --git a/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java b/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java deleted file mode 100644 index ce6360a..0000000 --- a/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.data.connector; - -/** - * Allows connecting to a SQLite Database. - * - * TODO implement - */ -public class DatabaseConnector { - -} diff --git a/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java new file mode 100644 index 0000000..6947c5d --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/SQLiteConnector.java @@ -0,0 +1,173 @@ +package de.rwu.easydrop.data.connector; + +import java.net.URL; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.sqlite.SQLiteDataSource; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.PersistenceException; + +/** + * Allows connecting to a SQLite Database. + * + * @since 0.2.0 + */ +public final class SQLiteConnector extends AbstractProductPersistence { + /** + * Data origin. + */ + private static final String DATA_ORIGIN = "SQLite"; + + /** + * SQLite Database. + */ + private SQLiteDataSource db; + + /** + * @param src the db to set + */ + public void setDb(final SQLiteDataSource src) { + this.db = src; + } + + /** + * Path to SQLite db file. + */ + private static final String PERSISTENCE_PATH = "jdbc:sqlite:persistence.db"; + + /** + * Creates instance. + * + * @param src SQLite Data Source + */ + public SQLiteConnector(final SQLiteDataSource src) { + db = src; + db.setUrl(PERSISTENCE_PATH); + initializeDatabase(); + } + + private void initializeDatabase() { + try { + // Create a new database connection + Connection connection = db.getConnection(); + + // Execute SQL statements to create tables + Statement statement = connection.createStatement(); + statement.execute( + "CREATE TABLE IF NOT EXISTS products (" + + "dataOrigin TEXT, " + + "productId TEXT, " + + "currentPrice REAL, " + + "merchant TEXT, " + + "deliveryPrice REAL, " + + "available INT, " + + "lastupdate TEXT, " + + "UNIQUE(productId, dataOrigin) ON CONFLICT REPLACE" + + ")"); + + // Close the statement and connection + statement.close(); + connection.close(); + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while initializing SQLite DB", e); + } + } + + /** + * Writes a ProductDTO to persistence. + * + * @param dto + */ + public void saveProduct(final ProductDTO dto) { + String query = "INSERT INTO products (" + + "dataOrigin, productId, currentPrice, merchant, " + + "deliveryPrice, available, lastupdate" + + ") VALUES (" + + "?, ?, ?, ?, ?, ?, datetime('now', 'localtime')" + + ")"; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + int index = 0; + + statement.setString(++index, dto.getDataOrigin()); + statement.setString(++index, dto.getProductId()); + statement.setDouble(++index, dto.getCurrentPrice()); + statement.setString(++index, dto.getMerchant()); + statement.setDouble(++index, dto.getDeliveryPrice()); + statement.setBoolean(++index, dto.isAvailable()); + + statement.executeUpdate(); + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while saving to SQLite", e); + } + } + + @Override + public ProductDTO getProductDTOById(final String productId) { + String query = "SELECT * FROM products WHERE productId = ?"; + ProductDTO dto = null; + + try (Connection connection = db.getConnection(); + PreparedStatement statement = connection.prepareStatement(query)) { + + statement.setString(1, productId); + + try (ResultSet resultSet = statement.executeQuery()) { + if (resultSet.next()) { + dto = new ProductDTO(resultSet.getString("productId"), + resultSet.getString("dataOrigin")); + dto.setCurrentPrice(resultSet.getDouble("currentPrice")); + dto.setMerchant(resultSet.getString("merchant")); + dto.setDeliveryPrice(resultSet.getDouble("deliveryPrice")); + dto.setAvailable(resultSet.getBoolean("available")); + } + } + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while reading from SQLite", e); + } + + return dto; + } + + /** + * Deletes all data from persistence. + */ + public void clearData() { + try (Connection connection = db.getConnection(); + Statement statement = connection.createStatement()) { + String query = "DELETE FROM products"; + statement.executeUpdate(query); + } catch (SQLException e) { + throw new PersistenceException("Something went wrong while clearing the database", e); + } + } + + @Override + protected String getDataOrigin() { + return DATA_ORIGIN; + } + + @Override + protected String getApiKey() { + throw new UnsupportedOperationException( + this.getClass().getName() + " doesn't support getApiKey"); + } + + @Override + protected ProductDTO buildProductDTO(final ProductDTO product, final String json) { + throw new UnsupportedOperationException( + this.getClass().getName() + " doesn't support buildProductDTO"); + } + + @Override + protected URL createApiUrl(final String productIdentifier) { + throw new UnsupportedOperationException( + this.getClass().getName() + " doesn't support createApiUrl"); + } +} diff --git a/src/main/java/de/rwu/easydrop/data/connector/package-info.java b/src/main/java/de/rwu/easydrop/data/connector/package-info.java index f5b2dc7..77ea22e 100644 --- a/src/main/java/de/rwu/easydrop/data/connector/package-info.java +++ b/src/main/java/de/rwu/easydrop/data/connector/package-info.java @@ -1,6 +1,6 @@ /** * Connectors for databases. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.data.connector; diff --git a/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java b/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java deleted file mode 100644 index f787da9..0000000 --- a/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.rwu.easydrop.data.dao; - -/** - * Product data access object. - * - * TODO implement - */ -public class ProductDAO { - -} diff --git a/src/main/java/de/rwu/easydrop/data/dao/package-info.java b/src/main/java/de/rwu/easydrop/data/dao/package-info.java deleted file mode 100644 index 2affd23..0000000 --- a/src/main/java/de/rwu/easydrop/data/dao/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Data access objects for business objects created from persistence. - * - * TODO implement - */ -package de.rwu.easydrop.data.dao; diff --git a/src/main/java/de/rwu/easydrop/data/package-info.java b/src/main/java/de/rwu/easydrop/data/package-info.java index a161322..dfbf299 100644 --- a/src/main/java/de/rwu/easydrop/data/package-info.java +++ b/src/main/java/de/rwu/easydrop/data/package-info.java @@ -1,6 +1,6 @@ /** * Structure for business objects and persisting their info. * - * TODO implement + * @since 0.2.0 */ package de.rwu.easydrop.data; diff --git a/src/main/java/de/rwu/easydrop/exception/PersistenceException.java b/src/main/java/de/rwu/easydrop/exception/PersistenceException.java new file mode 100644 index 0000000..0e32528 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/exception/PersistenceException.java @@ -0,0 +1,27 @@ +package de.rwu.easydrop.exception; + +/** + * Exception that signifies a problem with data persistence. + * + * @since 0.2.0 + */ +public class PersistenceException extends RuntimeException { + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + */ + public PersistenceException(final String message) { + super(message); + } + + /** + * Throws an exception that signifies the data of a Product are invalid. + * + * @param message + * @param cause + */ + public PersistenceException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java index 1b37a11..123080a 100644 --- a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java +++ b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java @@ -1,16 +1,17 @@ package de.rwu.easydrop.service.mapping; import de.rwu.easydrop.api.dto.ProductDTO; + import de.rwu.easydrop.model.Product; /** - * Maps between Product, ProductDAO and ProductDTO. + * Maps between Product, ProductDTO and ProductDTO. * * @since 0.2.0 * * @see Product * @see ProductDTO - * @see ProductDAO + * @see ProductDTO */ public final class ProductMapper { @@ -41,4 +42,21 @@ public final class ProductMapper { return product; } + + /** + * Creates a ProductDTO object from a corresponding Product. + * + * @param product Product + * @return ProductDTO + */ + public static ProductDTO mapProductToDTO(final Product product) { + ProductDTO dto = new ProductDTO(product.getProductId(), product.getDataOrigin()); + + dto.setAvailable(product.isAvailable()); + dto.setCurrentPrice(product.getCurrentPrice()); + dto.setDeliveryPrice(product.getDeliveryPrice()); + dto.setMerchant(product.getMerchant()); + + return dto; + } } diff --git a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java index e800938..64bb8c0 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/CatalogueRetriever.java @@ -5,6 +5,7 @@ import java.util.List; import javax.naming.ConfigurationException; +import de.rwu.easydrop.exception.InvalidProductException; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.util.ProductsConfig; @@ -60,10 +61,12 @@ public class CatalogueRetriever { newProduct.setDataOrigin(product.getDataOrigin()); newProduct.setProductId(product.getProductId()); - if (product.getDataOrigin().equals("Amazon")) { + if (newProduct.getDataOrigin().equals("Amazon")) { newProduct = productRetriever.getProductFromAmazon(product.getProductId()); - } else if (product.getDataOrigin().equals("eBay")) { + } else if (newProduct.getDataOrigin().equals("eBay")) { newProduct = productRetriever.getProductFromEbay(product.getProductId()); + } else { + throw new InvalidProductException("Product data origin is invalid"); } newProductCatalogue.addProduct(newProduct); diff --git a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java index bce5d10..da842c3 100644 --- a/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java +++ b/src/main/java/de/rwu/easydrop/service/retriever/ProductRetriever.java @@ -4,6 +4,7 @@ import de.rwu.easydrop.api.client.AmazonProductDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.service.mapping.ProductMapper; import de.rwu.easydrop.service.validation.ProductValidator; @@ -64,4 +65,20 @@ public class ProductRetriever { return product; } + + /** + * Retrieves a product from persistence. + * + * @param productId + * @return Product from persistence + */ + public Product getProductFromPersistence(final String productId) { + AbstractProductPersistence src = dataSourceFactory.createProductPersistenceDataSource(); + + ProductDTO dto = src.getProductDTOById(productId); + Product product = ProductMapper.mapProductFromDTO(dto); + ProductValidator.validate(product); + + return product; + } } diff --git a/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java b/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java new file mode 100644 index 0000000..a1cf486 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/CatalogueWriter.java @@ -0,0 +1,47 @@ +package de.rwu.easydrop.service.writer; + +import java.util.List; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; +import de.rwu.easydrop.service.mapping.ProductMapper; +import de.rwu.easydrop.service.validation.ProductValidator; + +/** + * Writes data for all products of multiple catalogues to persistence. + * + * @since 0.2.0 + */ +public final class CatalogueWriter { + /** + * Holds a persistence reference. + */ + private AbstractProductPersistence persistence; + + /** + * Creates new instance. + * + * @param newPersistence + */ + public CatalogueWriter(final AbstractProductPersistence newPersistence) { + persistence = newPersistence; + } + + /** + * Writes all products of specified catalogues to persistence. + * + * @param catalogues + */ + public void writeCatalogues(final List catalogues) { + for (ProductCatalogue pCat : catalogues) { + for (Product product : pCat.getProducts()) { + ProductValidator.validate(product); + ProductDTO dto = ProductMapper.mapProductToDTO(product); + + persistence.saveProduct(dto); + } + } + } +} diff --git a/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java b/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java new file mode 100644 index 0000000..d1be850 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/ProductWriter.java @@ -0,0 +1,38 @@ +package de.rwu.easydrop.service.writer; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.service.mapping.ProductMapper; +import de.rwu.easydrop.service.validation.ProductValidator; + +/** + * Wrapper for writing product info to persistence. + * + * @since 0.2.0 + */ +public class ProductWriter { + /** + * Persistence. + */ + private AbstractProductPersistence persistence; + + /** + * @param newPersistence the persistence to set + */ + public void setPersistence(final AbstractProductPersistence newPersistence) { + this.persistence = newPersistence; + } + + /** + * Validates and saves product to persistence. + * + * @param product + */ + public void writeProductToPersistence(final Product product) { + ProductValidator.validate(product); + ProductDTO dto = ProductMapper.mapProductToDTO(product); + + persistence.saveProduct(dto); + } +} diff --git a/src/main/java/de/rwu/easydrop/service/writer/package-info.java b/src/main/java/de/rwu/easydrop/service/writer/package-info.java new file mode 100644 index 0000000..09c58f8 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/writer/package-info.java @@ -0,0 +1,6 @@ +/** + * Writes Objects to a data store. + * + * @since 0.2.0 + */ +package de.rwu.easydrop.service.writer; diff --git a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java index c84df2a..f1c85b0 100644 --- a/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java +++ b/src/test/java/de/rwu/easydrop/api/client/DataSourceFactoryTest.java @@ -1,6 +1,8 @@ package de.rwu.easydrop.api.client; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; import javax.naming.ConfigurationException; @@ -9,7 +11,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.sqlite.SQLiteDataSource; +import de.rwu.easydrop.data.connector.SQLiteConnector; +import de.rwu.easydrop.exception.PersistenceException; import de.rwu.easydrop.util.Config; class DataSourceFactoryTest { @@ -45,4 +50,20 @@ class DataSourceFactoryTest { // Assert assertEquals("ebay-api-key", dataSource.getApiKey()); } + + @Test + void createProductPersistenceDataSource_NullPersistence() { + PersistenceException exception = assertThrows(PersistenceException.class, () -> { + dataSourceFactory.createProductPersistenceDataSource(); + }); + + assertEquals("Persistence is not set", exception.getMessage()); + } + + @Test + void createProductPersistenceDataSource_WorkingPersistence() { + dataSourceFactory.setPersistence(new SQLiteConnector(new SQLiteDataSource())); + + assertDoesNotThrow(() -> dataSourceFactory.createProductPersistenceDataSource()); + } } diff --git a/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java new file mode 100644 index 0000000..2e54299 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/data/connector/SQLiteConnectorTest.java @@ -0,0 +1,195 @@ +package de.rwu.easydrop.data.connector; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doThrow; + +import java.sql.SQLException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sqlite.SQLiteDataSource; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.exception.PersistenceException; + +@TestInstance(Lifecycle.PER_CLASS) +class SQLiteConnectorTest { + private static final String TEST_PRODUCT_ID = "12345"; + private SQLiteConnector sqliteConnector; + + @Mock + private SQLiteDataSource mockDataSource; + + @BeforeAll + public void setup() { + sqliteConnector = new SQLiteConnector(new SQLiteDataSource()); + } + + @BeforeEach + public void clearDatabase() { + MockitoAnnotations.openMocks(this); + } + + @Test + void saveProduct_ValidProduct_SuccessfullySaved() { + // Arrange + sqliteConnector.clearData(); + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + ProductDTO.setDataOrigin("Amazon"); + ProductDTO.setProductId(TEST_PRODUCT_ID); + ProductDTO.setCurrentPrice(9.99); + ProductDTO.setMerchant("Sample Merchant"); + ProductDTO.setDeliveryPrice(2.50); + ProductDTO.setAvailable(true); + + // Act + assertDoesNotThrow(() -> sqliteConnector.saveProduct(ProductDTO)); + + // Assert + ProductDTO savedProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID); + assertNotNull(savedProductDTO); + assertEquals("Amazon", savedProductDTO.getDataOrigin()); + assertEquals(TEST_PRODUCT_ID, savedProductDTO.getProductId()); + assertEquals(9.99, savedProductDTO.getCurrentPrice()); + assertEquals("Sample Merchant", savedProductDTO.getMerchant()); + assertEquals(2.50, savedProductDTO.getDeliveryPrice()); + assertTrue(savedProductDTO.isAvailable()); + } + + @Test + void getProductDTOById_ProductExists_ReturnsProductDTO() { + // Arrange + sqliteConnector.clearData(); + insertSampleProduct(); + + // Act + ProductDTO ProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID); + + // Assert + assertNotNull(ProductDTO); + assertEquals("Amazon", ProductDTO.getDataOrigin()); + assertEquals(TEST_PRODUCT_ID, ProductDTO.getProductId()); + assertEquals(9.99, ProductDTO.getCurrentPrice()); + assertEquals("Sample Merchant", ProductDTO.getMerchant()); + assertEquals(2.50, ProductDTO.getDeliveryPrice()); + assertTrue(ProductDTO.isAvailable()); + } + + @Test + void constructor_ThrowsPersistenceException_OnSQLException() { + try { + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + PersistenceException exception = assertThrows(PersistenceException.class, () -> { + new SQLiteConnector(mockDataSource); + }); + + assertEquals("Something went wrong while initializing SQLite DB", exception.getMessage()); + } catch (SQLException e) { + fail("No SQLException should be thrown"); + } + } + + @Test + void getProductDTOById_ProductDoesNotExist_ReturnsNull() { + // Act + ProductDTO ProductDTO = sqliteConnector.getProductDTOById("FAKE_ID"); + + // Assert + assertNull(ProductDTO); + } + + @Test + void saveProduct_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + assertThrows(PersistenceException.class, () -> sqliteConnector.saveProduct(ProductDTO)); + } + + @Test + void getProductDTOById_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + String productId = "12345"; + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + assertThrows(PersistenceException.class, () -> sqliteConnector.getProductDTOById(productId)); + } + + @Test + void clearData_ThrowsPersistenceException_OnSQLException() throws SQLException { + // Arrange + sqliteConnector.setDb(mockDataSource); + doThrow(SQLException.class).when(mockDataSource).getConnection(); + + // Act and Assert + assertThrows(PersistenceException.class, () -> sqliteConnector.clearData()); + } + + private void insertSampleProduct() { + ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon"); + ProductDTO.setCurrentPrice(9.99); + ProductDTO.setMerchant("Sample Merchant"); + ProductDTO.setDeliveryPrice(2.50); + ProductDTO.setAvailable(true); + sqliteConnector.saveProduct(ProductDTO); + } + + @Test + void getDataOrigin_ReturnsCorrectDataOrigin() { + // Arrange + SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + + // Act + String dataOrigin = connector.getDataOrigin(); + + // Assert + assertEquals("SQLite", dataOrigin); + } + + @Test + void getApiKey_UnsupportedOperationExceptionThrown() { + // Arrange + SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + + // Act and Assert + assertThrows(UnsupportedOperationException.class, connector::getApiKey); + } + + @Test + void buildProductDTO_UnsupportedOperationExceptionThrown() { + // Arrange + SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + ProductDTO product = new ProductDTO("ASIN123", "Amazon"); + String json = "{\"productId\":\"ASIN123\",\"dataOrigin\":\"Amazon\"}"; + + // Act and Assert + assertThrows(UnsupportedOperationException.class, () -> connector.buildProductDTO(product, json)); + } + + @Test + void createApiUrl_UnsupportedOperationExceptionThrown() { + // Arrange + SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource()); + String productIdentifier = "ASIN123"; + + // Act and Assert + assertThrows(UnsupportedOperationException.class, () -> connector.createApiUrl(productIdentifier)); + } +} diff --git a/src/test/java/de/rwu/easydrop/exception/PersistenceExceptionTest.java b/src/test/java/de/rwu/easydrop/exception/PersistenceExceptionTest.java new file mode 100644 index 0000000..b5d2ae2 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/exception/PersistenceExceptionTest.java @@ -0,0 +1,28 @@ +package de.rwu.easydrop.exception; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +class PersistenceExceptionTest { + + @Test + void testPersistenceExceptionWithMessage() { + String errorMessage = "Error occurred during data persistence."; + PersistenceException exception = new PersistenceException(errorMessage); + + assertEquals(errorMessage, exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testPersistenceExceptionWithMessageAndCause() { + String errorMessage = "Error occurred during data persistence."; + Throwable cause = new IllegalArgumentException("Invalid argument."); + PersistenceException exception = new PersistenceException(errorMessage, cause); + + assertEquals(errorMessage, exception.getMessage()); + assertEquals(cause, exception.getCause()); + } +} diff --git a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java index 6dbf741..6239dbf 100644 --- a/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java +++ b/src/test/java/de/rwu/easydrop/service/mapping/ProductMapperTest.java @@ -11,6 +11,7 @@ import java.lang.reflect.Modifier; import org.junit.jupiter.api.Test; import de.rwu.easydrop.api.dto.ProductDTO; + import de.rwu.easydrop.model.Product; class ProductMapperTest { @@ -53,4 +54,27 @@ class ProductMapperTest { dto.setMerchant("Example Merchant"); return dto; } + + @Test + void mapProductToDTO() { + // Arrange + Product product = new Product(); + product.setProductId("12345"); + product.setDataOrigin("Amazon"); + product.setAvailable(true); + product.setCurrentPrice(9.99); + product.setDeliveryPrice(2.50); + product.setMerchant("Seller1"); + + // Act + ProductDTO dto = ProductMapper.mapProductToDTO(product); + + // Assert + assertEquals("12345", dto.getProductId()); + assertEquals("Amazon", dto.getDataOrigin()); + assertTrue(dto.isAvailable()); + assertEquals(9.99, dto.getCurrentPrice()); + assertEquals(2.50, dto.getDeliveryPrice()); + assertEquals("Seller1", dto.getMerchant()); + } } diff --git a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java index a0721e9..acaca80 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/CatalogueRetrieverTest.java @@ -1,6 +1,7 @@ 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; @@ -13,6 +14,7 @@ import javax.naming.ConfigurationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import de.rwu.easydrop.exception.InvalidProductException; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.model.ProductCatalogue; import de.rwu.easydrop.util.ProductsConfig; @@ -40,11 +42,11 @@ class CatalogueRetrieverTest { product1.setDataOrigin("Amazon"); product1.setProductId("ASIN1"); productCatalogue.addProduct(product1); + Product product2 = new Product(); product2.setDataOrigin("eBay"); product2.setProductId("ProductID2"); productCatalogue.addProduct(product2); - productCatalogues.add(productCatalogue); // Mock the methods @@ -72,4 +74,30 @@ class CatalogueRetrieverTest { verify(productRetriever).getProductFromAmazon("ASIN1"); verify(productRetriever).getProductFromEbay("ProductID2"); } + + @Test + void loadCatalogues_ValidConfig_CataloguesLoaded_InvalidProduct() throws ConfigurationException { + // Arrange + List productCatalogues = new ArrayList<>(); + + // Create a sample product catalogue + ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue"); + + Product product = new Product(); + product.setDataOrigin(""); + product.setProductId("ProductID1"); + productCatalogue.addProduct(product); + productCatalogues.add(productCatalogue); + + // Mock the methods + when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues); + when(productRetriever.getProductFromAmazon("ProductID1")).thenReturn(product); + + // Act and Assert + InvalidProductException exception = assertThrows(InvalidProductException.class, () -> { + catalogueRetriever.loadCatalogues(); + }); + + assertEquals("Product data origin is invalid", exception.getMessage()); + } } diff --git a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java index 36dff9b..32afecc 100644 --- a/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java +++ b/src/test/java/de/rwu/easydrop/service/retriever/ProductRetrieverTest.java @@ -17,6 +17,7 @@ import de.rwu.easydrop.api.client.AmazonProductDataSource; import de.rwu.easydrop.api.client.DataSourceFactory; import de.rwu.easydrop.api.client.EbayItemDataSource; import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; import de.rwu.easydrop.model.Product; import de.rwu.easydrop.util.Config; @@ -33,6 +34,8 @@ class ProductRetrieverTest { private ProductDTO productDTO; @Mock private Product product; + @Mock + private AbstractProductPersistence persistence; private ProductRetriever productRetriever; @@ -84,4 +87,25 @@ class ProductRetrieverTest { assertEquals(9.99, result.getCurrentPrice()); verify(ebayDataSource, times(1)).getProductDTOById(productQuery); } + + @Test + void getProductFromPersistence_ValidProductId_ReturnsProduct() { + // Arrange + String productId = "123"; + when(dataSourceFactory.createProductPersistenceDataSource()).thenReturn(persistence); + when(persistence.getProductDTOById(productId)).thenReturn(productDTO); + when(productDTO.getProductId()).thenReturn(productId); + when(productDTO.getCurrentPrice()).thenReturn(9.99); + when(productDTO.getDataOrigin()).thenReturn("Amazon"); + + // Act + Product result = productRetriever.getProductFromPersistence(productId); + + // Assert + assertEquals(productId, result.getProductId()); + assertEquals(9.99, result.getCurrentPrice()); + + // Verify the interactions + verify(persistence, times(1)).getProductDTOById(productId); + } } diff --git a/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java new file mode 100644 index 0000000..71fdff5 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/writer/CatalogueWriterTest.java @@ -0,0 +1,69 @@ +package de.rwu.easydrop.service.writer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Product; +import de.rwu.easydrop.model.ProductCatalogue; + +class CatalogueWriterTest { + + @Mock + private AbstractProductPersistence persistenceMock; + + private CatalogueWriter catalogueWriter; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + catalogueWriter = new CatalogueWriter(persistenceMock); + } + + @Test + void writeCatalogues_ValidCatalogues_ProductsWrittenToPersistence() { + // Arrange + List catalogues = createSampleCatalogues(); + + // Act + catalogueWriter.writeCatalogues(catalogues); + + // Assert + verify(persistenceMock, times(4)).saveProduct(any(ProductDTO.class)); + } + + private List createSampleCatalogues() { + List catalogues = new ArrayList<>(); + + ProductCatalogue catalogue1 = new ProductCatalogue("Catalogue 1", "Sample catalogue 1"); + catalogue1.addProduct(createSampleProduct("Amazon", "ID 1")); + catalogue1.addProduct(createSampleProduct("eBay", "ID 2")); + + ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue 2", "Sample catalogue 2"); + catalogue2.addProduct(createSampleProduct("Amazon", "ID 3")); + catalogue2.addProduct(createSampleProduct("eBay", "ID 4")); + + catalogues.add(catalogue1); + catalogues.add(catalogue2); + + return catalogues; + } + + private Product createSampleProduct(String dataOrigin, String productId) { + Product product = new Product(); + product.setDataOrigin(dataOrigin); + product.setProductId(productId); + product.setCurrentPrice(9999.99); + return product; + } +} diff --git a/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java new file mode 100644 index 0000000..aab0cdd --- /dev/null +++ b/src/test/java/de/rwu/easydrop/service/writer/ProductWriterTest.java @@ -0,0 +1,55 @@ +package de.rwu.easydrop.service.writer; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import de.rwu.easydrop.api.dto.ProductDTO; +import de.rwu.easydrop.data.connector.AbstractProductPersistence; +import de.rwu.easydrop.model.Product; + +class ProductWriterTest { + + @Mock + private AbstractProductPersistence persistence; + + private ProductWriter productWriter; + + @BeforeEach + public void setup() { + MockitoAnnotations.openMocks(this); + productWriter = new ProductWriter(); + productWriter.setPersistence(persistence); + } + + @Test + void writeProductToPersistence_ValidProduct_CallsSaveProduct() { + // Arrange + Product product = new Product(); + product.setProductId("12345"); + product.setDataOrigin("Amazon"); + product.setCurrentPrice(9.99); + + // Act + productWriter.writeProductToPersistence(product); + + // Assert + Mockito.verify(persistence).saveProduct(any(ProductDTO.class)); + } + + @Test + void writeProductToPersistence_InvalidProduct_ThrowsException() { + // Arrange + Product product = new Product(); + product.setProductId(""); + product.setDataOrigin("Amazon"); + + // Act and Assert + assertThrows(Exception.class, () -> productWriter.writeProductToPersistence(product)); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java b/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java index b848b17..a415933 100644 --- a/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java +++ b/src/test/java/de/rwu/easydrop/util/ProductsConfigIntegrationTest.java @@ -1,14 +1,19 @@ package de.rwu.easydrop.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; +import java.util.List; + import javax.naming.ConfigurationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import de.rwu.easydrop.model.ProductCatalogue; + class ProductsConfigIntegrationTest { private ProductsConfig config; private final static String TESTDATA_PATH = "src/test/resources/test.products-config.json"; @@ -65,4 +70,20 @@ class ProductsConfigIntegrationTest { assertEquals("Products config is empty or malformed", exception.getMessage()); } + + @Test + void testReset() { + try { + config.setConfigLocation(TESTDATA_PATH); + config.loadConfig(); + } catch (ConfigurationException e) { + fail("ConfigurationException should not be thrown"); + } + + assertNotNull(config.getProductCatalogues()); + config.reset(); + List pCats = config.getProductCatalogues(); + + assertEquals(0, pCats.size(), "Catalogues list should be empty"); + } } From 450090e71a6ec6ea810eeecf95c3bf14bb5c2494 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 7 Jun 2023 23:21:57 +0200 Subject: [PATCH 34/37] #53 Updated changelog --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d62761a..71620ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,14 @@ ### Added - `EbayItemDataSource` (#37) +- Product data persistence via SQLite integration (#53) - Mutation testing capabilities via PITest (#66) - `Product` class - Mapping between Product and its corresponding DTO - Dependencies: Lombok for easier notation, JUnit 5 Params for repetitive tests -- Product Validation -- Product Retrieval -- Unit Tests +- Product validation, retrieval and persistence writing +- Product catalogue retrieval and persistence writing +- More unit tests ### Changed From 5fcfbe7a8bb1999c5a3b4aa624262fe429c85a07 Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Wed, 7 Jun 2023 23:30:11 +0200 Subject: [PATCH 35/37] #44 Removed needless text files --- Docs/Brainstorming Anforderungen | 20 -------------------- Docs/Definition of Done | 8 -------- Docs/Developing rules | 5 ----- Docs/To Do's (Notiz) | 5 ----- 4 files changed, 38 deletions(-) delete mode 100644 Docs/Brainstorming Anforderungen delete mode 100644 Docs/Definition of Done delete mode 100644 Docs/Developing rules delete mode 100644 Docs/To Do's (Notiz) diff --git a/Docs/Brainstorming Anforderungen b/Docs/Brainstorming Anforderungen deleted file mode 100644 index dbd6d1c..0000000 --- a/Docs/Brainstorming Anforderungen +++ /dev/null @@ -1,20 +0,0 @@ -Dropshipping - -Amazon vs. ebay - -software schreiben die ermittelt, ob ein produkt bei amazon oder ebay günstiger bei einer der beiden ist und dann automatisch teurer beim anderen einstellen -ziel: keine lagerkosten, profit durch marge -problem: kann natürlich sein, dass es zwischenzeitlich beim einen händler nicht mehr verfügbar ist wenn der kunde es bei uns kaufen will, -sodass der automatische kauf bei der günstigeren plattform nicht mehr ausgelöst werden kann -->angebot muss also automatisch rausgenommen werden, -sobald produkt bei der günstigeren plattform nicht mehr verfügbar ist -- preise vergleichen automatisiert -- verfügbarkeit vergleichen automatisiert -- bspw. verkauf des günstigen produktes auf drittplattform (etsy) => Shopify? - -welche anforderungen gibt es an mein produkt -- plattform/en: Amazon und eBay -- schnittstellen/APIs (zugang, kosten (pro aufruf)) -- absicherung (fallback falls ebay/amazon nicht liefert -->alibaba) -- welche Art der Datenbank -- Lagerhaltung? =>eher nein -- algorithmus erstellen der soziale Medien nach Hype-Produkten durchsucht =>affiliate links? diff --git a/Docs/Definition of Done b/Docs/Definition of Done deleted file mode 100644 index b56b13e..0000000 --- a/Docs/Definition of Done +++ /dev/null @@ -1,8 +0,0 @@ -- Code should be free of faults -- Tests should be green by 90% -- Code is pushed to Team Repository -- Code has only few comments -- No commented out Code in Software -- Code is documented -- Code is executable on server -- Project is presented to customer / product owner / professor diff --git a/Docs/Developing rules b/Docs/Developing rules deleted file mode 100644 index 5af1467..0000000 --- a/Docs/Developing rules +++ /dev/null @@ -1,5 +0,0 @@ -- Everything from code to comment is written in English; documentation may be in German -- Every developer has only one story in development. If the developer needs to halt with development in their story due to unforeseen reasons, they may already start with another story. -- Every branch works on one story -- User stories should be written in a clear way so that every developer can understand in what development status the ticket is -- Another rule diff --git a/Docs/To Do's (Notiz) b/Docs/To Do's (Notiz) deleted file mode 100644 index 3bb56e9..0000000 --- a/Docs/To Do's (Notiz) +++ /dev/null @@ -1,5 +0,0 @@ -- Welche Datenbank? AWS ja nein? =>Friedrich fragen (Rocketchat) -- Welche Werkzeuge? VSC -- Sprache: Java -- Kein Frontend -- From ff7ccfede624ea1cb7e4ec36b1aa3196add85c6e Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Thu, 8 Jun 2023 00:25:16 +0200 Subject: [PATCH 36/37] Bumped version to 0.2.0 --- CHANGELOG.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71620ef..ad4b782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.2.0-SNAPSHOT +## 0.2.0 ### Added diff --git a/pom.xml b/pom.xml index 58603a6..9361980 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ de.rwu easydrop jar - 0.2.0-SNAPSHOT + 0.2.0 EasyDrop http://maven.apache.org From 3f36fc0428e4535ef3d8cd9d0fd6aa091d2fd68e Mon Sep 17 00:00:00 2001 From: Marvin Scham Date: Thu, 8 Jun 2023 00:25:33 +0200 Subject: [PATCH 37/37] Created proper README --- Docs/DemoImage.png | Bin 0 -> 23760 bytes README.md | 105 ++++++++++++--------------------------------- 2 files changed, 28 insertions(+), 77 deletions(-) create mode 100644 Docs/DemoImage.png diff --git a/Docs/DemoImage.png b/Docs/DemoImage.png new file mode 100644 index 0000000000000000000000000000000000000000..18fb11fc7985ee0aa591f2f5bd039208ebe1c997 GIT binary patch literal 23760 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 3bcc903..067a701 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,43 @@ -# Sandbox2 +# EasyDrop - - -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin http://gitlab.fbe-adswen.rwu.de/team1/sandbox2.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](http://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. +![Version](https://img.shields.io/badge/version-0.2.0-blue) +![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) ## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +Get rich quick™ with fully automated dropshipping! -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +## Visuals ✨ -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +![Demo Image](Docs/DemoImage.png) -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +## Installation ⚙ -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +As the application is still in development, development software will be required along its regular execution environment. -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +- JDK 17 +- Maven +- _[More/context](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/wikis/Projektdokumentation/Development#software)_ -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +## Usage 🛠 -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +Set up the required configuration files, use the corresponding demo files for orientation. -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +- `config/config.properties` for API authorization +- `products-config.json` to define product catalogues to use for dropshipping -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +Run the following command to execute the program: -## License -For open source projects, say how it is licensed. +```sh +mvn compile exec:java -Dexec.mainClass="de.rwu.easydrop.Main" +``` -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +## Roadmap 🏁 + +_Future ideas!_ + +## Contributing 👷‍♂️👷‍♀️ + +Contribution guidelines are available in the [project wiki](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/wikis/Richtlinien/Development)