Merge branch '#37-eBay-Product-API' into 'dev'

eBay Product API

See merge request team1/sandbox2!3
This commit is contained in:
Marvin Scham
2023-06-06 00:19:25 +00:00
46 changed files with 1473 additions and 225 deletions

View File

@@ -1,3 +1,8 @@
{ {
"java.configuration.updateBuildConfiguration": "automatic" "java.configuration.updateBuildConfiguration": "automatic",
} "sonarlint.connectedMode.project": {
"connectionId": "LocalSonarQube",
"projectKey": "EasyDrop"
},
"java.debug.settings.onBuildFailureProceed": true
}

View File

@@ -1,10 +1,34 @@
# Changelog # Changelog
## 0.2.0-SNAPSHOT
### Added
- `EbayItemDataSource` (#37)
- `Product` class
- Mapping between Product and its corresponding DTO
- Dependencies: Lombok for easier notation, JUnit 5 Params for repetitive tests
- Product Validation
- Product Retrieval
- Tests
### Changed
- `AbstractDataSource` between DataSource interface and implementations to minimize code duplication
- Affects AmazonProductDataSource and related tests (~ #39)
- Testing resources moved to more intuitive location
- Demo `Main` method now uses proper Product retrieval
### Fixed
- Logback configuration is now linked correctly
- Config now properly adheres to singleton pattern
## 0.1.0 ## 0.1.0
### Added ### Added
- (DataSource) → AmazonProductDataSource as first external data source (#39) - (DataSource) → `AmazonProductDataSource` as first external data source (#39)
- ProductDTO as data holder structure for data from external sources (~ #39) - `ProductDTO` as data holder structure for data from external sources (~ #39)
- Config for credential, base URLs etc. (~ #39) - Config for credential, base URLs etc. (~ #39)
- FormattingUtil for formatting price strings (~ #39) - FormattingUtil for formatting price strings (~ #39)

View File

@@ -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"
}
]
}

View File

@@ -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

View File

@@ -1,3 +1,5 @@
# Amazon Credentials # Amazon Credentials
AMAZON_API_URL= AMAZON_API_URL=
AMAZON_API_KEY= AMAZON_API_KEY=
EBAY_API_URL=
EBAY_API_KEY=

2
lombok.config Normal file
View File

@@ -0,0 +1,2 @@
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true

19
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>de.rwu</groupId> <groupId>de.rwu</groupId>
<artifactId>easydrop</artifactId> <artifactId>easydrop</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
<version>0.1.0</version> <version>0.2.0-SNAPSHOT</version>
<name>EasyDrop</name> <name>EasyDrop</name>
<url>http://maven.apache.org</url> <url>http://maven.apache.org</url>
@@ -16,16 +16,29 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>junit-jupiter-api</artifactId>
<version>5.8.0</version> <version>5.9.3</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>
<version>5.8.0</version> <version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.3</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>

View File

@@ -5,7 +5,8 @@ import javax.naming.ConfigurationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import de.rwu.easydrop.api.client.AmazonProductDataSource; import de.rwu.easydrop.api.client.DataSourceFactory;
import de.rwu.easydrop.service.retriever.ProductRetriever;
import de.rwu.easydrop.util.Config; import de.rwu.easydrop.util.Config;
/** /**
@@ -33,17 +34,12 @@ public final class Main {
*/ */
public static void main(final String[] args) throws ConfigurationException { public static void main(final String[] args) throws ConfigurationException {
Config config = Config.getInstance(); Config config = Config.getInstance();
config.loadConfig(); DataSourceFactory dataSourceFactory = new DataSourceFactory(config);
String amznBaseUrl = config.getProperty("AMAZON_API_URL"); ProductRetriever retriever = new ProductRetriever(dataSourceFactory);
String amznApiKey = config.getProperty("AMAZON_API_KEY");
String testProduct = null;
AmazonProductDataSource amznSrc = new AmazonProductDataSource(amznBaseUrl, amznApiKey); String amznProduct = retriever.getProductFromAmazon("B096Y2TYKV").toString();
try { LOGGER.info(amznProduct);
testProduct = amznSrc.getProductDTOById("B096Y2TYKV").toString(); String ebayProduct = retriever.getProductFromEbay("Gigabyte GeForce RTX 3060").toString();
LOGGER.info(testProduct); LOGGER.info(ebayProduct);
} catch (IllegalArgumentException e) {
LOGGER.error("Something went wrong :(", e);
}
} }
} }

View File

@@ -0,0 +1,100 @@
package de.rwu.easydrop.api.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.exception.DataSourceException;
import de.rwu.easydrop.util.FormattingUtil;
/**
* Helper class for shared data and functions between data sources.
*
* @since 0.2.0
*/
public abstract class AbstractDataSource implements DataSource {
/**
* Returns the data origin for the current source.
*
* @return Data source name
*/
protected abstract String getDataOrigin();
/**
* Returns the data source's API key.
*
* @return Data source API key
*/
protected abstract String getApiKey();
/**
* Enriches a ProductDTO with API-gathered data.
*
* @param product Unfinished ProductDTO
* @param json Product data
* @return Finished ProductDTO
*/
protected abstract ProductDTO buildProductDTO(ProductDTO product, String json);
/**
* Overridable standard implementation.
*/
@Override
public ProductDTO getProductDTOById(final String productIdentifier)
throws IllegalArgumentException {
StringBuilder response = new StringBuilder();
String dataOrigin = getDataOrigin();
String apiKey = getApiKey();
ProductDTO product = new ProductDTO(productIdentifier, dataOrigin);
try {
String urlReadyIdentifier = FormattingUtil.urlEncode(productIdentifier);
URL apiUrl = createApiUrl(urlReadyIdentifier);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Credential", apiKey);
int responseCode = connection.getResponseCode();
BufferedReader reader;
if (responseCode == HttpURLConnection.HTTP_OK) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
throw new DataSourceException(
"Nothing found: "
+ dataOrigin
+ " API responded with error code "
+ responseCode);
}
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
buildProductDTO(product, response.toString());
} catch (IOException e) {
throw new DataSourceException(
"Couldn't fulfill "
+ dataOrigin
+ " API request");
}
return product;
}
/**
* Creates an URL object to connect to the API with.
*
* @param productIdentifier Product identifier
* @return URL object
* @throws MalformedURLException
*/
protected abstract URL createApiUrl(String productIdentifier) throws MalformedURLException;
}

View File

@@ -1,9 +1,5 @@
package de.rwu.easydrop.api.client; 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.MalformedURLException;
import java.net.URL; import java.net.URL;
@@ -18,7 +14,7 @@ import de.rwu.easydrop.api.dto.ProductDTO;
* *
* @since 0.1.0 * @since 0.1.0
*/ */
public final class AmazonProductDataSource implements DataSource { public final class AmazonProductDataSource extends AbstractDataSource {
/** /**
* Name of this data source. * Name of this data source.
*/ */
@@ -28,7 +24,7 @@ public final class AmazonProductDataSource implements DataSource {
*/ */
private String baseUrl; private String baseUrl;
/** /**
* Credential key to authorize acccess. * Credential key to authorize access.
*/ */
private String apiKey; private String apiKey;
/** /**
@@ -52,48 +48,7 @@ public final class AmazonProductDataSource implements DataSource {
} }
@Override @Override
public ProductDTO getProductDTOById(final String productId) throws IllegalArgumentException { protected ProductDTO buildProductDTO(final ProductDTO product, final String json) {
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."; String root = "$.featuredOffer.";
ReadContext ctx = JsonPath.parse(json); ReadContext ctx = JsonPath.parse(json);
@@ -112,8 +67,11 @@ public final class AmazonProductDataSource implements DataSource {
return product; return product;
} }
/**
* @param productId ASIN
*/
@Override @Override
public URL createApiUrl(final String productId) throws MalformedURLException { protected URL createApiUrl(final String productId) throws MalformedURLException {
return new URL(baseUrl return new URL(baseUrl
+ "/products/2020-08-26/products/" + "/products/2020-08-26/products/"
+ productId + productId
@@ -122,4 +80,14 @@ public final class AmazonProductDataSource implements DataSource {
+ "&locale=" + "&locale="
+ LOCALE); + LOCALE);
} }
@Override
protected String getDataOrigin() {
return DATA_ORIGIN;
}
@Override
protected String getApiKey() {
return this.apiKey;
}
} }

View File

@@ -1,8 +1,5 @@
package de.rwu.easydrop.api.client; package de.rwu.easydrop.api.client;
import java.net.MalformedURLException;
import java.net.URL;
import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.api.dto.ProductDTO;
/** /**
@@ -12,17 +9,8 @@ public interface DataSource {
/** /**
* Retrieves product info from the data source. * Retrieves product info from the data source.
* *
* @param productId ASIN * @param productIdentifier Product identifier
* @return ProductDTO * @return ProductDTO
*/ */
ProductDTO getProductDTOById(String productId); ProductDTO getProductDTOById(String productIdentifier);
/**
* Creates an URL object to connect to the API with.
*
* @param productId ASIN
* @return URL object
* @throws MalformedURLException
*/
URL createApiUrl(String productId) throws MalformedURLException;
} }

View File

@@ -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);
}
}

View File

@@ -0,0 +1,88 @@
package de.rwu.easydrop.api.client;
import java.net.MalformedURLException;
import java.net.URL;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import com.jayway.jsonpath.ReadContext;
import de.rwu.easydrop.api.dto.ProductDTO;
/**
* Interface to an eBay data source.
*
* @since 0.2.0
*/
public final class EbayItemDataSource extends AbstractDataSource {
/**
* Name of this data source.
*/
private static final String DATA_ORIGIN = "eBay";
/**
* Base URL to the eBay data source.
*/
private String baseUrl;
/**
* Credential key to authorize access.
*/
private String apiKey;
/**
* Sets up instance with Base URL and API Key.
*
* @param newBaseUrl
* @param newApiKey
*/
public EbayItemDataSource(final String newBaseUrl, final String newApiKey) {
this.baseUrl = newBaseUrl;
this.apiKey = newApiKey;
}
/**
* @param searchQuery Exact product name or other valid identifier.
*/
@Override
protected URL createApiUrl(final String searchQuery) throws MalformedURLException {
return new URL(baseUrl
+ "/buy/browse/v1/item_summary/search?q="
+ searchQuery
+ "&limit=1&offset=0");
}
/**
* Enriches a ProductDTO with API-gathered data.
*
* @param product Unfinished ProductDTO
* @param json Product data
* @return Finished ProductDTO
*/
protected ProductDTO buildProductDTO(final ProductDTO product, final String json) {
String root = "$.itemSummaries[0].";
ReadContext ctx = JsonPath.parse(json);
try {
product.setDataOrigin(DATA_ORIGIN);
product.setAvailable(
ctx.read(root + "shippingOptions[0].guaranteedDelivery", boolean.class));
product.setCurrentPrice(ctx.read(root + "price.value", double.class));
product.setDeliveryPrice(
ctx.read(root + "shippingOptions[0].shippingCost.value", double.class));
product.setMerchant(ctx.read(root + "seller.username", String.class));
} catch (PathNotFoundException e) {
// Pass, allow incomplete ProductDTO to pass for later validation
}
return product;
}
@Override
protected String getDataOrigin() {
return DATA_ORIGIN;
}
@Override
protected String getApiKey() {
return this.apiKey;
}
}

View File

@@ -1,139 +1,52 @@
package de.rwu.easydrop.api.dto; package de.rwu.easydrop.api.dto;
import de.rwu.easydrop.util.FormattingUtil; import lombok.Data;
/** /**
* Product data transfer object. * Product data transfer object.
* *
* @since 0.1.0 * @since 0.1.0
*/ */
@Data
public class ProductDTO { public class ProductDTO {
/** /**
* Data source platform, like "Amazon". * Data source platform, like "Amazon".
*/ */
private String dataOrigin; 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. * Platform internal product identifier.
*/ */
private String productId; private String productId;
/**
* @return the productId
*/
public String getProductId() {
return productId;
}
/** /**
* Current product price per piece in Euro. * Current product price per piece in Euro.
*/ */
private double currentPrice; 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. * Name of mercant offering the product on the platform.
*/ */
private String merchant; 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. * Additional Cost for delivery in Euro.
*/ */
private double deliveryPrice; 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. * Whether the product can be purchased at this point.
*/ */
private boolean available; 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. * Creates ProductDTO instance.
* *
* @param newProductId Interal Product indetifier * @param newProductId Internal Product indetifier
* @param newDataOrigin Data Origin * @param newDataOrigin Data Origin
*/ */
public ProductDTO(final String newProductId, final String newDataOrigin) { public ProductDTO(final String newProductId, final String newDataOrigin) {
this.productId = newProductId; this.productId = newProductId;
this.dataOrigin = newDataOrigin; this.dataOrigin = newDataOrigin;
} }
@Override
public final String toString() {
return "ProductDTO{"
+ productId + " from "
+ merchant + " ("
+ dataOrigin + ")"
+ " at "
+ FormattingUtil.formatEuro(currentPrice) + " (available: "
+ (available ? "yes" : "no") + ")}";
}
} }

View File

@@ -1,10 +0,0 @@
package de.rwu.easydrop.data.model;
/**
* A Product.
*
* TODO implement
*/
public class Product {
}

View File

@@ -1,6 +0,0 @@
/**
* Business objects.
*
* TODO implement
*/
package de.rwu.easydrop.data.model;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,6 @@
/**
* Contains EasyDrop-related custom exceptions.
*
* @since 0.2.0
*/
package de.rwu.easydrop.exception;

View File

@@ -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") + ")]";
}
}

View File

@@ -0,0 +1,6 @@
/**
* Business objects.
*
* @since 0.2.0
*/
package de.rwu.easydrop.model;

View File

@@ -1,14 +1,44 @@
package de.rwu.easydrop.service.mapping; 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, ProductDAO and ProductDTO.
* *
* TODO implement * @since 0.2.0
* *
* @see Product * @see Product
* @see ProductDTO * @see ProductDTO
* @see ProductDAO * @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;
}
} }

View File

@@ -1,6 +1,6 @@
/** /**
* Maps different formats of corresponding objects. * Maps different formats of corresponding objects.
* *
* TODO implement * @since 0.2.0
*/ */
package de.rwu.easydrop.service.mapping; package de.rwu.easydrop.service.mapping;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
/**
* Retrieves Objects from a data source.
*
* @since 0.2.0
*/
package de.rwu.easydrop.service.retriever;

View File

@@ -1,10 +1,56 @@
package de.rwu.easydrop.service.validation; 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. * 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<String> validOrigins = new HashSet<>();
validOrigins.add("Amazon");
validOrigins.add("eBay");
return validOrigins.contains(dataOrigin);
}
} }

View File

@@ -1,6 +1,6 @@
/** /**
* Supports validation processes. * Supports validation processes.
* *
* TODO implement * @since 0.2.0
*/ */
package de.rwu.easydrop.service.validation; package de.rwu.easydrop.service.validation;

View File

@@ -56,7 +56,7 @@ public final class Config {
*/ */
public static Config getInstance() { public static Config getInstance() {
if (instance == null) { if (instance == null) {
return new Config(); instance = new Config();
} }
return instance; return instance;
@@ -109,4 +109,11 @@ public final class Config {
public void setProperty(final String key, final String value) { public void setProperty(final String key, final String value) {
properties.setProperty(key, value); properties.setProperty(key, value);
} }
/**
* Resets the config's properties.
*/
public void reset() {
properties = null;
}
} }

View File

@@ -27,4 +27,14 @@ public final class FormattingUtil {
public static String formatEuro(final double amount) { public static String formatEuro(final double amount) {
return String.format(Locale.GERMAN, "%,.2f", 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(" ", "+");
}
} }

View File

@@ -1,4 +1,6 @@
<configuration> <configuration>
<logger name="com.jayway.jsonpath" level="OFF" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
@@ -6,7 +8,7 @@
</encoder> </encoder>
</appender> </appender>
<root level="info"> <root level="INFO">
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />
</root> </root>
</configuration> </configuration>

View File

@@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import de.rwu.easydrop.api.dto.ProductDTO; import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.exception.DataSourceException;
class AmazonProductDataSourceTest { class AmazonProductDataSourceTest {
@@ -150,6 +151,7 @@ class AmazonProductDataSourceTest {
AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class);
URL mockURL = mock(URL.class); URL mockURL = mock(URL.class);
when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin);
when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL);
when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod();
HttpURLConnection mockConnection = mock(HttpURLConnection.class); HttpURLConnection mockConnection = mock(HttpURLConnection.class);
@@ -157,7 +159,7 @@ class AmazonProductDataSourceTest {
when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND);
// Invoke the method and verify the exception // Invoke the method and verify the exception
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { DataSourceException exception = assertThrows(DataSourceException.class, () -> {
dataSource.getProductDTOById(demoProductId); dataSource.getProductDTOById(demoProductId);
}); });
@@ -170,6 +172,7 @@ class AmazonProductDataSourceTest {
// Set up the test environment // Set up the test environment
AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class);
URL mockURL = mock(URL.class); URL mockURL = mock(URL.class);
when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin);
when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL);
when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod();
when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod(); when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod();
@@ -179,11 +182,23 @@ class AmazonProductDataSourceTest {
when(mockConnection.getInputStream()).thenThrow(new IOException()); when(mockConnection.getInputStream()).thenThrow(new IOException());
// Invoke the method and verify the exception // Invoke the method and verify the exception
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { DataSourceException exception = assertThrows(DataSourceException.class, () -> {
dataSource.getProductDTOById(demoProductId); dataSource.getProductDTOById(demoProductId);
}); });
// Verify the exception message // Verify the exception message
assertEquals("Couldn't fulfill Amazon API request", exception.getMessage()); assertEquals("Couldn't fulfill Amazon API request", exception.getMessage());
} }
@Test
void getDataOrigin_ReturnsExpectedDataOrigin() {
String dataOrigin = demoDataSource.getDataOrigin();
assertEquals(demoDataOrigin, dataOrigin);
}
@Test
void getApiKey_ReturnsExpectedApiKey() {
String apiKey = demoDataSource.getApiKey();
assertEquals(demoApiKey, apiKey);
}
} }

View File

@@ -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());
}
}

View File

@@ -0,0 +1,125 @@
package de.rwu.easydrop.api.client;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.exception.DataSourceException;
class EbayItemDataSourceTest {
private EbayItemDataSource demoDataSource;
private static String demoApiKey = "my-api-key";
private static String demoApiUrl = "https://www.example.com/api";
private static String demoDataOrigin = "eBay";
private static String demoQuery = "iPhone";
@BeforeEach
void setUp() {
demoDataSource = new EbayItemDataSource(demoApiUrl, demoApiKey);
MockitoAnnotations.openMocks(this);
}
@Test
void testConstructor() {
// Assert
try {
Field baseUrlField = EbayItemDataSource.class.getDeclaredField("baseUrl");
baseUrlField.setAccessible(true);
Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoDataSource));
Field apiKeyField = EbayItemDataSource.class.getDeclaredField("apiKey");
apiKeyField.setAccessible(true);
Assertions.assertEquals(demoApiKey, apiKeyField.get(demoDataSource));
} catch (NoSuchFieldException e) {
Assertions.fail();
} catch (IllegalAccessException e) {
Assertions.fail();
}
}
@Test
void createApiUrl_ValidSearchQuery_ReturnsValidUrl() throws MalformedURLException {
URL apiUrl = demoDataSource.createApiUrl(demoQuery);
assertNotNull(apiUrl);
assertEquals("https://www.example.com/api/buy/browse/v1/item_summary/search?q=iPhone&limit=1&offset=0",
apiUrl.toString());
}
@Test
void buildProductDTO_ValidJson_ReturnsValidProductDTO() {
ProductDTO product = new ProductDTO(demoQuery, demoDataOrigin);
String json = "{\"itemSummaries\":[{\"shippingOptions\":[{\"guaranteedDelivery\":true,\"shippingCost\":{\"value\":10}}],\"price\":{\"value\":999.99},\"seller\":{\"username\":\"seller123\"}}]}";
ProductDTO result = demoDataSource.buildProductDTO(product, json);
assertEquals(demoDataOrigin, result.getDataOrigin());
assertEquals(true, result.isAvailable());
assertEquals(999.99, result.getCurrentPrice());
assertEquals(10.0, result.getDeliveryPrice());
assertEquals("seller123", result.getMerchant());
}
@Test
void buildProductDTO_InvalidJson_ReturnsProductDTOWithDefaults() {
ProductDTO product = new ProductDTO(demoQuery, demoDataOrigin);
String json = "{\"itemSummaries\":[]}"; // Empty JSON to simulate missing data
ProductDTO result = demoDataSource.buildProductDTO(product, json);
assertEquals("eBay", result.getDataOrigin());
assertEquals(false, result.isAvailable()); // Default value for boolean
assertEquals(0.0, result.getCurrentPrice()); // Default value for double
assertEquals(0.0, result.getDeliveryPrice()); // Default value for double
assertEquals(null, result.getMerchant()); // Default value for String
}
@Test
void getDataOrigin_ReturnsExpectedDataOrigin() {
String dataOrigin = demoDataSource.getDataOrigin();
assertEquals(demoDataOrigin, dataOrigin);
}
@Test
void getApiKey_ReturnsExpectedApiKey() {
String apiKey = demoDataSource.getApiKey();
assertEquals(demoApiKey, apiKey);
}
@Test
void testGetProductDTOById_failedRequest() throws IOException {
// Set up the test environment
EbayItemDataSource dataSource = mock(EbayItemDataSource.class);
URL mockURL = mock(URL.class);
when(dataSource.getDataOrigin()).thenReturn(demoDataOrigin);
when(dataSource.createApiUrl(demoQuery)).thenReturn(mockURL);
when(dataSource.getProductDTOById(demoQuery)).thenCallRealMethod();
HttpURLConnection mockConnection = mock(HttpURLConnection.class);
when(mockURL.openConnection()).thenReturn(mockConnection);
when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND);
// Invoke the method and verify the exception
DataSourceException exception = assertThrows(DataSourceException.class, () -> {
dataSource.getProductDTOById(demoQuery);
});
// Verify the exception message
assertEquals("Nothing found: eBay API responded with error code 404", exception.getMessage());
}
}

View File

@@ -1,33 +1,56 @@
package de.rwu.easydrop.api.dto; package de.rwu.easydrop.api.dto;
import static org.junit.jupiter.api.Assertions.assertEquals; 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; import org.junit.jupiter.api.Test;
class ProductDTOTest { class ProductDTOTest {
@Test @Test
void testToString1() { void constructor_SetsProductIdAndDataOrigin() {
ProductDTO product1 = new ProductDTO("12345", "Amazon"); // Arrange
product1.setMerchant("Merchant A"); String productId = "12345";
product1.setCurrentPrice(19.99); String dataOrigin = "Amazon";
product1.setAvailable(true);
String expectedString1 = "ProductDTO{12345 from Merchant A (Amazon) at 19,99 € (available: yes)}"; // Act
String result1 = product1.toString(); ProductDTO productDTO = new ProductDTO(productId, dataOrigin);
assertEquals(expectedString1, result1); // Assert
assertEquals(productId, productDTO.getProductId());
assertEquals(dataOrigin, productDTO.getDataOrigin());
} }
@Test @Test
void testToString2() { void gettersAndSetters_WorkAsExpected() {
ProductDTO product2 = new ProductDTO("67890", "eBay"); // Arrange
product2.setMerchant("Merchant B"); ProductDTO productDTO = new ProductDTO("12345", "Amazon");
product2.setCurrentPrice(9.99);
product2.setAvailable(false);
String expectedString2 = "ProductDTO{67890 from Merchant B (eBay) at 9,99 € (available: no)}"; // Act and Assert
String result2 = product2.toString(); 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());
} }
} }

View File

@@ -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");
});
}
}

View File

@@ -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");
});
}
}

View File

@@ -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());
}
}

View File

@@ -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<ProductMapper> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<ProductValidator> 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<Product> 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;
}
}

View File

@@ -1,6 +1,7 @@
package de.rwu.easydrop.util; package de.rwu.easydrop.util;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.assertThrows;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@@ -13,7 +14,7 @@ import org.junit.jupiter.api.Test;
class ConfigImplTest { class ConfigImplTest {
private Config config; 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_KEY = "API_KEY";
private final static String TESTDATA_VAL = "keyIsHere"; private final static String TESTDATA_VAL = "keyIsHere";
@@ -34,7 +35,9 @@ class ConfigImplTest {
} }
@Test @Test
void testGetProperty_ConfigNotLoaded() { void testGetProperty_ConfigNotLoaded() throws Exception {
config.reset();
NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> { NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> {
config.getProperty(TESTDATA_KEY); config.getProperty(TESTDATA_KEY);
}); });
@@ -74,7 +77,7 @@ class ConfigImplTest {
@Test @Test
void testLoadConfigSuccessfully() { void testLoadConfigSuccessfully() {
try { try {
config.setConfigLocation("testResources/testdata.properties"); config.setConfigLocation("src/test/resources/testdata.properties");
config.loadConfig(); config.loadConfig();
assertEquals(TESTDATA_VAL, config.getProperty(TESTDATA_KEY)); assertEquals(TESTDATA_VAL, config.getProperty(TESTDATA_KEY));
} catch (ConfigurationException e) { } catch (ConfigurationException e) {
@@ -92,4 +95,19 @@ class ConfigImplTest {
assertEquals("Couldn't load required config file", exception.getMessage()); 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());
}
} }

View File

@@ -2,6 +2,7 @@ package de.rwu.easydrop.util;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import javax.naming.ConfigurationException; import javax.naming.ConfigurationException;
@@ -20,17 +21,18 @@ class ConfigTest {
} }
@Test @Test
void testGetInstanceNull() { void testGetInstance() {
config = null;
Config newConfig = Config.getInstance(); Config newConfig = Config.getInstance();
assertNotNull(newConfig); assertNotNull(newConfig);
} }
@Test @Test
void testGetInstanceNotNull() { void testGetInstanceEquality() {
Config newConfig = Config.getInstance(); // Create "two" instances to check validity of Singleton pattern
assertNotNull(newConfig); Config instance1 = Config.getInstance();
Config instance2 = Config.getInstance();
assertSame(instance1, instance2, "Instances should be equal");
} }
@Test @Test

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,5 @@
API_KEY=keyIsHere
AMAZON_API_URL=THE_AMAZON_API_URL
AMAZON_API_KEY=THE_AMAZON_API_KEY
EBAY_API_URL=THE_EBAY_API_URL
EBAY_API_KEY=THE_EBAY_API_KEY

View File

@@ -1 +0,0 @@
API_KEY=keyIsHere