diff --git a/.gitignore b/.gitignore index 3d4bc4d..24affe0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +################################################################################################### +## Project Internal ############################################################################### +################################################################################################### + +config.properties + ################################################################################################### ## Visual Studio Code ############################################################################# ################################################################################################### diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a5b7a3..010c0fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ # Changelog -## Use me! \ No newline at end of file +## 0.1.0 + +### Added + +- (DataSource) → AmazonProductDataSource as first external data source (#39) +- ProductDTO as data holder structure for data from external sources (~ #39) +- Config for credential, base URLs etc. (~ #39) +- FormattingUtil for formatting price strings (~ #39) diff --git a/SampleResponses/AmazonProductAPIModel.json b/SampleResponses/AmazonProductAPIModel.json new file mode 100644 index 0000000..0d0cd3a --- /dev/null +++ b/SampleResponses/AmazonProductAPIModel.json @@ -0,0 +1,544 @@ +{ + "/products/2020-08-26/products/{productId}/offers": { + "get": { + "produces": ["application/json"], + "parameters": [ + { + "description": "The Amazon Standard Item Identifier (ASIN) for the product.", + "in": "path", + "name": "productId", + "required": true, + "type": "string" + }, + { + "description": "The region where the customer wants to purchase the product.", + "enum": ["DE", "FR", "UK", "IT", "ES", "US", "CA", "JP"], + "x-docgen-enum-table-extension": [ + { + "value": "DE", + "description": "Germany" + }, + { + "value": "FR", + "description": "France" + }, + { + "value": "UK", + "description": "United Kingdom" + }, + { + "value": "IT", + "description": "Italy" + }, + { + "value": "ES", + "description": "Spain" + }, + { + "value": "US", + "description": "United States of America" + }, + { + "value": "CA", + "description": "Canada" + }, + { + "value": "JP", + "description": "Japan" + } + ], + "in": "query", + "name": "productRegion", + "required": true, + "type": "string" + }, + { + "description": "This field determines the region where to ship the product based on the value in the shippingPostalCode.", + "in": "query", + "name": "shippingRegion", + "required": false, + "type": "string" + }, + { + "description": "The locale of the request, in the form of an IETF language tag. Each ProductRegion supports one or more locales. This value should comply with java.util.Locale.", + "pattern": "^[-_]+$", + "in": "query", + "name": "locale", + "required": true, + "type": "string" + }, + { + "description": "The shipping postal or zip code for customer's request. This parameter doesn't hold geocode.", + "in": "query", + "name": "shippingPostalCode", + "required": false, + "type": "string" + }, + { + "description": "The zero-based number of the page being requested. If not specified, a default value of 0 will be used. When passed, the value must be equal or greater than zero, and less than the number of pages returned in the response.", + "in": "query", + "name": "pageNumber", + "required": false, + "type": "integer" + }, + { + "description": "The number of items desired for each page in the response. If not specified, a default value of 24 will be used. Maximum items that can be fetched in single request is 24.", + "in": "query", + "name": "pageSize", + "required": false, + "type": "integer" + }, + { + "description": "A value that identifies the group within the business account that a customer belongs to. The customer can set this in Amazon Business account information. GroupTag is necessary only if the customer account belongs to more than one group.", + "in": "query", + "name": "groupTag", + "required": false, + "type": "string" + }, + { + "collectionFormat": "csv", + "description": "A list of filter ids for use to query results.", + "in": "query", + "name": "filterIds", + "required": false, + "type": "array", + "items": { + "type": "string" + } + }, + { + "description": "The number of units the customer intends to purchase. This helps Amazon Business determines quantity-based discounts if an eligible offer is present. Defaults to 1.", + "in": "query", + "name": "quantity", + "required": false, + "type": "integer", + "default": 1 + }, + { + "description": "The email address of the customer requesting this resource", + "in": "header", + "name": "x-amz-user-email", + "required": true, + "type": "string" + }, + { + "collectionFormat": "csv", + "description": "A list specifying the offer fields for inclusion in the response object. Excluding this query parameter results in the response including all inclusions.", + "enum": [ + "ALL", + "availability", + "buyingGuidance", + "buyingRestrictions", + "condition", + "fulfillmentType", + "merchant", + "price", + "productCondition", + "productConditionNote", + "quantityLimits", + "quantityPrice", + "taxExclusivePrice", + "deliveryInformation", + "listPrice", + "shippingOptions" + ], + "in": "query", + "name": "inclusionsForOffers", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "x-example": "/path?inclusionsForOffers=FOO,BAR" + } + ], + "responses": { + "200": { + "description": "Search success", + "schema": { + "$ref": "#/definitions/OffersResult" + }, + "headers": { + "x-amzn-RateLimit-Limit": { + "description": "The rate limit (requests per second) for this operation.", + "type": "string" + }, + "x-amzn-requestid": { + "description": "A unique request reference identifier.", + "type": "string" + } + }, + "x-amazon-spds-sandbox-behaviors": [ + { + "request": { + "parameters": { + "productRegion": { + "value": "DE" + }, + "locale": { + "value": "es_US" + }, + "productId": { + "value": "B07VDJ5RVF" + }, + "pageNumber": { + "value": "1" + }, + "pageSize": { + "value": "12" + }, + "filterIds": { + "value": ["NEW", "PRIME"] + } + } + }, + "response": { + "offerCount": 2, + "numberOfPages": 1, + "featuredOffer": { + "availability": "[availability]", + "buyingGuidance": "NONE", + "deliveryInformation": "[deliveryInformation]", + "fulfillmentType": "AMAZON_FULFILLMENT", + "offerId": "[offerId]", + "merchant": { + "name": "[merchantName]", + "merchantId": "[merchantId]", + "meanFeedbackRating": 5.0, + "totalFeedbackCount": 1 + }, + "price": { + "value": { + "amount": 1.0, + "currencyCode": "USD" + }, + "formattedPrice": "[formattedPrice]", + "priceType": "NEW" + }, + "listPrice": { + "value": { + "amount": 2.0, + "currencyCode": "USD" + }, + "formattedPrice": "[formattedPrice]", + "priceType": "NEW" + }, + "taxExclusivePrice": { + "taxExclusiveAmount": { + "amount": 1.0, + "currencyCode": "USD" + }, + "formattedPrice": "[formattedPrice]", + "displayString": "[taxExclusivePriceDisplayString]", + "label": "[taxExclusivePriceLabel]" + }, + "quantityLimits": { + "minQuantity": 1, + "maxQuantity": 6 + }, + "quantityPrice": { + "quantityPriceTiers": [ + { + "quantityDisplay": "string", + "unitPrice": { + "amount": 0, + "currencyCode": "USD" + }, + "minQuantity": 0, + "price": { + "value": { + "amount": 0, + "currencyCode": "USD" + }, + "formattedPrice": "[formattedPrice]", + "priceType": "NEW" + }, + "savingMessage": "string", + "taxExclusivePrice": { + "taxExclusiveAmount": { + "amount": 0, + "currencyCode": "USD" + }, + "displayString": "[taxExclusivePriceDisplayString]", + "formattedPrice": "[formattedPrice]", + "label": "[taxExclusivePriceLabel]" + } + } + ] + }, + "productCondition": "NEW", + "productConditionNote": "[productConditionNote]", + "condition": { + "conditionValue": "NEW", + "conditionNote": "string", + "subCondition": "ACCEPTABLE" + }, + "buyingRestrictions": [], + "shippingOptions": [ + { + "shippingCost": { + "value": { + "amount": 9.0, + "currencyCode": "USD" + } + }, + "deliveryRange": { + "max": "2022-01-01T00:00:01.000Z", + "min": "2022-01-01T00:00:01.000Z" + } + } + ] + }, + "offers": [ + { + "offerId": "[offerId]", + "merchant": { + "name": "[merchantName]", + "merchantId": "[merchantId]", + "meanFeedbackRating": 5.0, + "totalFeedbackCount": 1 + }, + "deliveryInformation": "[deliveryInformation]", + "price": { + "value": { + "amount": 1.0, + "currencyCode": "USD" + }, + "formattedPrice": "[formattedPrice]", + "priceType": "NEW" + }, + "listPrice": { + "value": { + "amount": 2.0, + "currencyCode": "USD" + }, + "formattedPrice": "[formattedPrice]", + "priceType": "NEW" + }, + "taxExclusivePrice": { + "taxExclusiveAmount": { + "amount": 1.0, + "currencyCode": "USD" + }, + "formattedPrice": "[formattedPrice]", + "displayString": "[taxExclusivePriceDisplayString]", + "label": "[taxExclusivePriceLabel]" + }, + "quantityLimits": { + "minQuantity": 1, + "maxQuantity": 6 + }, + "quantityPrice": { + "quantityPriceTiers": [ + { + "quantityDisplay": "string", + "unitPrice": { + "amount": 0, + "currencyCode": "USD" + }, + "minQuantity": 0, + "price": { + "value": { + "amount": 0, + "currencyCode": "USD" + }, + "formattedPrice": "[formattedPrice]", + "priceType": "NEW" + }, + "savingMessage": "string", + "taxExclusivePrice": { + "taxExclusiveAmount": { + "amount": 0, + "currencyCode": "USD" + }, + "displayString": "[taxExclusivePriceDisplayString]", + "formattedPrice": "[formattedPrice]", + "label": "[taxExclusivePriceLabel]" + } + } + ] + }, + "productCondition": "NEW", + "productConditionNote": "[productConditionNote]", + "condition": { + "conditionValue": "NEW", + "conditionNote": "string", + "subCondition": "ACCEPTABLE" + }, + "availability": "[availability]", + "fulfillmentType": "THIRD_PARTY", + "buyingGuidance": "NONE", + "buyingRestrictions": [], + "shippingOptions": [ + { + "shippingCost": { + "value": { + "amount": 9.0, + "currencyCode": "USD" + } + }, + "deliveryRange": { + "max": "2022-01-01T00:00:01.000Z", + "min": "2022-01-01T00:00:01.000Z" + } + } + ] + } + ], + "filterGroups": [ + { + "displayName": "[displayName]", + "items": [ + { + "displayName": "[itemDisplayName]", + "id": "[itemId]" + } + ] + } + ] + } + } + ] + }, + "400": { + "description": "The service was unable to process the request. Reasons for the error are described in an error response object.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + }, + "headers": { + "x-amzn-RateLimit-Limit": { + "description": "The rate limit (requests per second) for this operation.", + "type": "string" + }, + "x-amzn-requestid": { + "description": "A unique request reference identifier.", + "type": "string" + } + }, + "x-amazon-spds-sandbox-behaviors": [ + { + "request": { + "parameters": { + "productRegion": { + "value": "US" + }, + "productId": { + "value": "B07VDJ5RVF" + } + } + }, + "response": { + "errors": [ + { + "code": "INVALID_REQUEST_PARAMETER", + "message": "Missing required request parameters: [locale]" + } + ] + } + } + ] + }, + "401": { + "description": "The request's authorization header isn't formatted correctly or doesn't contain a valid authorization input.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + }, + "headers": { + "x-amzn-RateLimit-Limit": { + "description": "The rate limit (requests per second) for this operation.", + "type": "string" + }, + "x-amzn-requestid": { + "description": "A unique request reference identifier.", + "type": "string" + } + } + }, + "403": { + "description": "Indicates that access to the resource is forbidden. Possible reasons include Access Denied, Unauthorized, Expired Token, or Invalid Signature.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + }, + "headers": { + "x-amzn-RateLimit-Limit": { + "description": "The rate limit (requests per second) for this operation.", + "type": "string" + }, + "x-amzn-requestid": { + "description": "A unique request reference identifier.", + "type": "string" + } + } + }, + "404": { + "description": "The resource specified doesn't exist.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + }, + "headers": { + "x-amzn-RateLimit-Limit": { + "description": "The rate limit (requests per second) for this operation.", + "type": "string" + }, + "x-amzn-requestid": { + "description": "A unique request reference identifier.", + "type": "string" + } + } + }, + "429": { + "description": "The frequency of requests was greater than allowed.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + }, + "headers": { + "x-amzn-RateLimit-Limit": { + "description": "The rate limit (requests per second) for this operation.", + "type": "string" + }, + "x-amzn-requestid": { + "description": "A unique request reference identifier.", + "type": "string" + } + } + }, + "500": { + "description": "Encountered an unexpected condition which prevented the server from fulfilling the request.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + }, + "headers": { + "x-amzn-RateLimit-Limit": { + "description": "The rate limit (requests per second) for this operation.", + "type": "string" + }, + "x-amzn-requestid": { + "description": "A unique request reference identifier.", + "type": "string" + } + } + }, + "503": { + "description": "Temporary overloading or maintenance of the server.", + "schema": { + "$ref": "#/definitions/ErrorResponse" + }, + "headers": { + "x-amzn-RateLimit-Limit": { + "description": "The rate limit (requests per second) for this operation.", + "type": "string" + }, + "x-amzn-requestid": { + "description": "A unique request reference identifier.", + "type": "string" + } + } + } + }, + "tags": ["search"], + "description": "Search for offers of a specific product.", + "operationId": "searchOffersRequest", + "summary": "Paginated and filtered search for offers of a specific product." + } + } +} diff --git a/config/custom-checkstyle.xml b/config/custom-checkstyle.xml new file mode 100644 index 0000000..71d9e97 --- /dev/null +++ b/config/custom-checkstyle.xml @@ -0,0 +1,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/demo.config.properties b/config/demo.config.properties new file mode 100644 index 0000000..fbb6e44 --- /dev/null +++ b/config/demo.config.properties @@ -0,0 +1,3 @@ +# Amazon Credentials +AMAZON_API_URL= +AMAZON_API_KEY= \ No newline at end of file diff --git a/config/logback.xml b/config/logback.xml new file mode 100644 index 0000000..6b610a9 --- /dev/null +++ b/config/logback.xml @@ -0,0 +1,12 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + UTF-8 + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 880badf..88f5f8e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,27 +5,58 @@ de.rwu easydrop jar - 0.1-SNAPSHOT + 0.1.0 EasyDrop http://maven.apache.org UTF-8 + 17 + 17 - junit - junit - 4.13.2 + org.junit.jupiter + junit-jupiter-api + 5.8.0 test + + org.junit.jupiter + junit-jupiter-engine + 5.8.0 + test + + org.mockito mockito-core 5.3.1 test + + + org.json + json + 20230227 + + + com.jayway.jsonpath + json-path + 2.8.0 + + + + org.slf4j + slf4j-api + 2.0.7 + + + ch.qos.logback + logback-classic + 1.4.7 + @@ -39,6 +70,7 @@ checkstyle-check verify + config/custom-checkstyle.xml true true true diff --git a/src/main/java/de/rwu/easydrop/Main.java b/src/main/java/de/rwu/easydrop/Main.java new file mode 100644 index 0000000..af1aeea --- /dev/null +++ b/src/main/java/de/rwu/easydrop/Main.java @@ -0,0 +1,49 @@ +package de.rwu.easydrop; + +import javax.naming.ConfigurationException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.rwu.easydrop.api.client.AmazonProductDataSource; +import de.rwu.easydrop.util.Config; + +/** + * Kickoff point for the service. + * + * @since 0.1.0 + */ +public final class Main { + /** + * Logger for main process. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); + + /** + * Prevents unwanted instantiation. + */ + private Main() throws IllegalAccessException { + throw new IllegalAccessException("Don't instantiate me! >:("); + } + + /** + * Application entrypoint. + * + * @param args + */ + 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; + + AmazonProductDataSource amznSrc = new AmazonProductDataSource(amznBaseUrl, amznApiKey); + try { + testProduct = amznSrc.getProductDTOById("B096Y2TYKV").toString(); + LOGGER.info(testProduct); + } catch (IllegalArgumentException e) { + LOGGER.error("Something went wrong :(", e); + } + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java new file mode 100644 index 0000000..dbbdc6d --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/AmazonProductDataSource.java @@ -0,0 +1,125 @@ +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 com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; +import com.jayway.jsonpath.ReadContext; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Interface to an Amazon data source. + * + * @since 0.1.0 + */ +public final class AmazonProductDataSource implements DataSource { + /** + * Name of this data source. + */ + private static final String DATA_ORIGIN = "Amazon"; + /** + * Base URL to the Amazon data source. + */ + private String baseUrl; + /** + * Credential key to authorize acccess. + */ + private String apiKey; + /** + * Product region parameter required for data access. + */ + private static final String PRODUCT_REGION = "DE"; + /** + * Locale parameter required for data access. + */ + private static final String LOCALE = "de_DE"; + + /** + * Sets up instance with Base URL and API Key. + * + * @param newBaseUrl + * @param newApiKey + */ + public AmazonProductDataSource(final String newBaseUrl, final String newApiKey) { + this.baseUrl = newBaseUrl; + this.apiKey = newApiKey; + } + + @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); + + try { + product.setDataOrigin(DATA_ORIGIN); + product.setAvailable( + ctx.read(root + "availability", String.class).equals("available")); + product.setCurrentPrice(ctx.read(root + "price.value.amount", double.class)); + product.setDeliveryPrice( + ctx.read(root + "shippingOptions[0].shippingCost.value.amount", double.class)); + product.setMerchant(ctx.read(root + "merchant.name", String.class)); + } catch (PathNotFoundException e) { + // Pass, allow incomplete ProductDTO to pass for later validation + } + + return product; + } + + @Override + public URL createApiUrl(final String productId) throws MalformedURLException { + return new URL(baseUrl + + "/products/2020-08-26/products/" + + productId + + "/offers?productRegion=" + + PRODUCT_REGION + + "&locale=" + + LOCALE); + } +} diff --git a/src/main/java/de/rwu/easydrop/api/client/DataSource.java b/src/main/java/de/rwu/easydrop/api/client/DataSource.java new file mode 100644 index 0000000..7b1c4e7 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/DataSource.java @@ -0,0 +1,28 @@ +package de.rwu.easydrop.api.client; + +import java.net.MalformedURLException; +import java.net.URL; + +import de.rwu.easydrop.api.dto.ProductDTO; + +/** + * Universal interface to implement concrete APIs on. + */ +public interface DataSource { + /** + * Retrieves product info from the data source. + * + * @param productId ASIN + * @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; +} diff --git a/src/main/java/de/rwu/easydrop/api/client/package-info.java b/src/main/java/de/rwu/easydrop/api/client/package-info.java new file mode 100644 index 0000000..b4040d6 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/client/package-info.java @@ -0,0 +1,6 @@ +/** + * API client connectors for interaction with external data sources. + * + * @since 0.1.0 + */ +package de.rwu.easydrop.api.client; diff --git a/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java new file mode 100644 index 0000000..e3a706c --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/dto/ProductDTO.java @@ -0,0 +1,139 @@ +package de.rwu.easydrop.api.dto; + +import de.rwu.easydrop.util.FormattingUtil; + +/** + * Product data transfer object. + * + * @since 0.1.0 + */ +public class ProductDTO { + /** + * Data source platform, like "Amazon". + */ + private String dataOrigin; + + /** + * @return the dataOrigin + */ + public String getDataOrigin() { + return dataOrigin; + } + + /** + * @param newDataOrigin the dataOrigin to set + */ + public void setDataOrigin(final String newDataOrigin) { + this.dataOrigin = newDataOrigin; + } + + /** + * Platform internal product identifier. + */ + private String productId; + + /** + * @return the productId + */ + public String getProductId() { + return productId; + } + + /** + * Current product price per piece in Euro. + */ + private double currentPrice; + + /** + * @return the currentPrice + */ + public double getCurrentPrice() { + return currentPrice; + } + + /** + * @param newCurrentPrice the currentPrice to set + */ + public void setCurrentPrice(final double newCurrentPrice) { + this.currentPrice = newCurrentPrice; + } + + /** + * Name of mercant offering the product on the platform. + */ + private String merchant; + + /** + * @return the merchant + */ + public String getMerchant() { + return merchant; + } + + /** + * @param newMerchant the merchant to set + */ + public void setMerchant(final String newMerchant) { + this.merchant = newMerchant; + } + + /** + * Additional Cost for delivery in Euro. + */ + private double deliveryPrice; + + /** + * @return the deliveryPrice + */ + public double getDeliveryPrice() { + return deliveryPrice; + } + + /** + * @param newDeliveryPrice the deliveryPrice to set + */ + public void setDeliveryPrice(final double newDeliveryPrice) { + this.deliveryPrice = newDeliveryPrice; + } + + /** + * Whether the product can be purchased at this point. + */ + private boolean available; + + /** + * @return the available + */ + public boolean isAvailable() { + return available; + } + + /** + * @param newAvailable the available to set + */ + public void setAvailable(final boolean newAvailable) { + this.available = newAvailable; + } + + /** + * Creates ProductDTO instance. + * + * @param newProductId Interal Product indetifier + * @param newDataOrigin Data Origin + */ + public ProductDTO(final String newProductId, final String newDataOrigin) { + this.productId = newProductId; + this.dataOrigin = newDataOrigin; + } + + @Override + public final String toString() { + return "ProductDTO{" + + productId + " from " + + merchant + " (" + + dataOrigin + ")" + + " at " + + FormattingUtil.formatEuro(currentPrice) + " (available: " + + (available ? "yes" : "no") + ")}"; + } +} diff --git a/src/main/java/de/rwu/easydrop/api/dto/package-info.java b/src/main/java/de/rwu/easydrop/api/dto/package-info.java new file mode 100644 index 0000000..7d4f621 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/dto/package-info.java @@ -0,0 +1,6 @@ +/** + * Data transfer objects for data derived from external sources. + * + * @since 0.1.0 + */ +package de.rwu.easydrop.api.dto; diff --git a/src/main/java/de/rwu/easydrop/api/package-info.java b/src/main/java/de/rwu/easydrop/api/package-info.java new file mode 100644 index 0000000..5c0f1cf --- /dev/null +++ b/src/main/java/de/rwu/easydrop/api/package-info.java @@ -0,0 +1,6 @@ +/** + * Interaction with external APIs. + * + * @since 0.1.0 + */ +package de.rwu.easydrop.api; diff --git a/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java b/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java new file mode 100644 index 0000000..ce6360a --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/DatabaseConnector.java @@ -0,0 +1,10 @@ +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/package-info.java b/src/main/java/de/rwu/easydrop/data/connector/package-info.java new file mode 100644 index 0000000..f5b2dc7 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/connector/package-info.java @@ -0,0 +1,6 @@ +/** + * Connectors for databases. + * + * TODO implement + */ +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 new file mode 100644 index 0000000..f787da9 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/dao/ProductDAO.java @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..2affd23 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/dao/package-info.java @@ -0,0 +1,6 @@ +/** + * 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/model/Product.java b/src/main/java/de/rwu/easydrop/data/model/Product.java new file mode 100644 index 0000000..cfee872 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/model/Product.java @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..f33ce31 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/model/package-info.java @@ -0,0 +1,6 @@ +/** + * Business objects. + * + * TODO implement + */ +package de.rwu.easydrop.data.model; diff --git a/src/main/java/de/rwu/easydrop/data/package-info.java b/src/main/java/de/rwu/easydrop/data/package-info.java new file mode 100644 index 0000000..a161322 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/data/package-info.java @@ -0,0 +1,6 @@ +/** + * Structure for business objects and persisting their info. + * + * TODO implement + */ +package de.rwu.easydrop.data; diff --git a/src/main/java/de/rwu/easydrop/demo/DemoClass.java b/src/main/java/de/rwu/easydrop/demo/DemoClass.java deleted file mode 100644 index ff91a4f..0000000 --- a/src/main/java/de/rwu/easydrop/demo/DemoClass.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.rwu.easydrop.demo; - -/** - * Demo-Class. - */ -public final class DemoClass { - /** - * Demo-Method inverting a boolean. - * - * @param bool Input Boolean - * @return Inverted Boolean - */ - public boolean invertBool(final boolean bool) { - return !bool; - } -} diff --git a/src/main/java/de/rwu/easydrop/demo/package-info.java b/src/main/java/de/rwu/easydrop/demo/package-info.java deleted file mode 100644 index 01c1a1b..0000000 --- a/src/main/java/de/rwu/easydrop/demo/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Dieses Paket beinhaltet ist zu Demo-Zwecken. - * - * @since 0.1-SNAPSHOT - */ -package de.rwu.easydrop.demo; diff --git a/src/main/java/de/rwu/easydrop/package-info.java b/src/main/java/de/rwu/easydrop/package-info.java new file mode 100644 index 0000000..9d9a0d1 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/package-info.java @@ -0,0 +1,6 @@ +/** + * One of the Top 2 dropshipping platforms around. + * + * @since 0.1.0 + */ +package de.rwu.easydrop; diff --git a/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java new file mode 100644 index 0000000..5d4a6d3 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/mapping/ProductMapper.java @@ -0,0 +1,14 @@ +package de.rwu.easydrop.service.mapping; + +/** + * Maps between Product, ProductDAO and ProductDTO. + * + * TODO implement + * + * @see Product + * @see ProductDTO + * @see ProductDAO + */ +public class ProductMapper { + +} 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 new file mode 100644 index 0000000..ab86051 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/mapping/package-info.java @@ -0,0 +1,6 @@ +/** + * Maps different formats of corresponding objects. + * + * TODO implement + */ +package de.rwu.easydrop.service.mapping; diff --git a/src/main/java/de/rwu/easydrop/service/package-info.java b/src/main/java/de/rwu/easydrop/service/package-info.java new file mode 100644 index 0000000..ff59e0c --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/package-info.java @@ -0,0 +1,6 @@ +/** + * Packages for supporting business logic. + * + * TODO implement + */ +package de.rwu.easydrop.service; diff --git a/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java new file mode 100644 index 0000000..4d373b2 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/processing/OrderManager.java @@ -0,0 +1,10 @@ +package de.rwu.easydrop.service.processing; + +/** + * Processes dropshipping orders. + * + * TODO implement + */ +public class OrderManager { + +} diff --git a/src/main/java/de/rwu/easydrop/service/processing/package-info.java b/src/main/java/de/rwu/easydrop/service/processing/package-info.java new file mode 100644 index 0000000..db7cb9e --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/processing/package-info.java @@ -0,0 +1,6 @@ +/** + * Supports diverse business processes and enforces business rules. + * + * TODO implement + */ +package de.rwu.easydrop.service.processing; diff --git a/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java new file mode 100644 index 0000000..3c93108 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/validation/ProductValidator.java @@ -0,0 +1,10 @@ +package de.rwu.easydrop.service.validation; + +/** + * Confirms validity of Product data. + * + * @since 0.1.0 + */ +public class ProductValidator { + +} 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 new file mode 100644 index 0000000..6b45561 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/service/validation/package-info.java @@ -0,0 +1,6 @@ +/** + * Supports validation processes. + * + * TODO implement + */ +package de.rwu.easydrop.service.validation; diff --git a/src/main/java/de/rwu/easydrop/util/Config.java b/src/main/java/de/rwu/easydrop/util/Config.java new file mode 100644 index 0000000..7a7f1e3 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/util/Config.java @@ -0,0 +1,112 @@ +package de.rwu.easydrop.util; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.Properties; + +import javax.naming.ConfigurationException; + +/** + * Allows access to the config. + * + * @since 0.1.0 + */ +public final class Config { + /** + * Config file location. + */ + private String configLocation = "config/config.properties"; + + /** + * @return the configLocation + */ + public String getConfigLocation() { + return configLocation; + } + + /** + * @param newConfigLocation the configLocation to set + */ + public void setConfigLocation(final String newConfigLocation) { + configLocation = newConfigLocation; + } + + /** + * Holds the config values. + */ + private Properties properties = null; + /** + * Singleton instance. + */ + private static Config instance = null; + + /** + * Private constructor to prevent external instantiation. + */ + private Config() { + // Do Nothing + } + + /** + * Returns current config instance. + * + * @return Config instance + * @throws ConfigurationException + */ + public static Config getInstance() { + if (instance == null) { + return new Config(); + } + + return instance; + } + + /** + * Loads config file values into the instance. + * + * @throws ConfigurationException + */ + public void loadConfig() throws ConfigurationException { + Properties newProps = new Properties(); + try (FileInputStream input = new FileInputStream(configLocation)) { + newProps.load(input); + properties = newProps; + } catch (IOException e) { + throw new ConfigurationException("Couldn't load required config file"); + } + } + + /** + * Returns a config property by specified key. + * + * @param key Config Key, like "API_KEY" + * @return Config value + * @throws NoSuchElementException Required key missing + */ + public String getProperty(final String key) throws NoSuchElementException { + String value = null; + + try { + value = properties.getProperty(key); + } catch (NullPointerException e) { + throw new NoSuchElementException("Config has not been loaded"); + } + + if (value == null) { + throw new NoSuchElementException("Requested config value does not exist"); + } + + return value; + } + + /** + * Overrides a config property loaded from file for current instance. + * + * @param key Config Key + * @param value Property Value + */ + public void setProperty(final String key, final String value) { + properties.setProperty(key, value); + } +} diff --git a/src/main/java/de/rwu/easydrop/util/FormattingUtil.java b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java new file mode 100644 index 0000000..283e506 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/util/FormattingUtil.java @@ -0,0 +1,30 @@ +package de.rwu.easydrop.util; + +import java.util.Locale; + +/** + * Helps format a bunch of things. + * + * @since 0.1.0 + */ +public final class FormattingUtil { + + /** + * Private constructor to prevent unwanted instantiation. + * + * @throws UnsupportedOperationException always + */ + private FormattingUtil() throws UnsupportedOperationException { + throw new UnsupportedOperationException("This is a utility class, don't instantiate it."); + } + + /** + * Formats a price to the German Euro format. + * + * @param amount price + * @return formatted price + */ + public static String formatEuro(final double amount) { + return String.format(Locale.GERMAN, "%,.2f", amount) + " €"; + } +} diff --git a/src/main/java/de/rwu/easydrop/util/package-info.java b/src/main/java/de/rwu/easydrop/util/package-info.java new file mode 100644 index 0000000..40789f2 --- /dev/null +++ b/src/main/java/de/rwu/easydrop/util/package-info.java @@ -0,0 +1,6 @@ +/** + * General utility such as formatting helpers. + * + * TODO implement + */ +package de.rwu.easydrop.util; diff --git a/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java new file mode 100644 index 0000000..032d682 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/client/AmazonProductDataSourceTest.java @@ -0,0 +1,189 @@ +package de.rwu.easydrop.api.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +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; + +class AmazonProductDataSourceTest { + + private AmazonProductDataSource demoDataSource; + + private static String demoApiKey = "my-api-key"; + private static String demoApiUrl = "https://www.example.com/api"; + private static String demoDataOrigin = "Amazon"; + private static String demoProductId = "whateverId"; + + @BeforeEach + void setup() { + demoDataSource = new AmazonProductDataSource( + demoApiUrl, + demoApiKey); + MockitoAnnotations.openMocks(this); + } + + @Test + void testConstructor() { + // Assert + try { + Field baseUrlField = AmazonProductDataSource.class.getDeclaredField("baseUrl"); + baseUrlField.setAccessible(true); + Assertions.assertEquals(demoApiUrl, baseUrlField.get(demoDataSource)); + + Field apiKeyField = AmazonProductDataSource.class.getDeclaredField("apiKey"); + apiKeyField.setAccessible(true); + Assertions.assertEquals(demoApiKey, apiKeyField.get(demoDataSource)); + } catch (NoSuchFieldException e) { + Assertions.fail(); + } catch (IllegalAccessException e) { + Assertions.fail(); + } + } + + @Test + void testCreateApiUrl() throws MalformedURLException { + // Test case 1 + String productId1 = "12345"; + URL expectedUrl1 = new URL( + "https://www.example.com/api/products/2020-08-26/products/12345/offers?productRegion=DE&locale=de_DE"); + URL createdUrl1 = demoDataSource.createApiUrl(productId1); + Assertions.assertEquals(expectedUrl1, createdUrl1); + + // Test case 2 + String productId2 = "67890"; + URL expectedUrl2 = new URL( + "https://www.example.com/api/products/2020-08-26/" + + "products/67890/offers?productRegion=DE&locale=de_DE"); + URL createdUrl2 = demoDataSource.createApiUrl(productId2); + Assertions.assertEquals(expectedUrl2, createdUrl2); + } + + @Test + void testBuildProductDTO_completeData() { + // Set up the test environment + String json = "{\"featuredOffer\": {\"availability\": \"available\"," + + "\"price\": {\"value\": {\"amount\": 10.0}}," + + "\"shippingOptions\": [{\"shippingCost\": {\"value\": {\"amount\": 2.5}}}], " + + "\"merchant\": {\"name\": \"Merchant A\"}}}"; + ProductDTO product = new ProductDTO(demoProductId, demoDataOrigin); + + // Invoke the method + ProductDTO result = demoDataSource.buildProductDTO(product, json); + + // Verify the product DTO properties + assertEquals(demoDataOrigin, result.getDataOrigin()); + assertEquals(true, result.isAvailable()); + assertEquals(10.0, result.getCurrentPrice()); + assertEquals(2.5, result.getDeliveryPrice()); + assertEquals("Merchant A", result.getMerchant()); + } + + @Test + void testBuildProductDTO_incompleteData() { + // Set up the test environment + String json = "{\"otherOffer\": {\"availability\": \"available\"," + + "\"price\": {\"value\": {\"amount\": 10.0}}," + + "\"shippingOptions\": [{\"shippingCost\": {\"value\": {\"amount\": 2.5}}}], " + + "\"merchant\": {\"name\": \"Merchant A\"}}}"; + ProductDTO product = new ProductDTO(demoProductId, demoDataOrigin); + + // Invoke the method + ProductDTO result = demoDataSource.buildProductDTO(product, json); + + // Verify that the product DTO remains unchanged + assertEquals(demoDataOrigin, result.getDataOrigin()); + assertEquals(false, result.isAvailable()); + assertEquals(0.0, result.getCurrentPrice()); + assertEquals(0.0, result.getDeliveryPrice()); + assertEquals(null, result.getMerchant()); + } + + @Test + void testGetProductDTOById_successfulRequest() throws IOException { + // Set up the test environment + String mockResponse = "{\"featuredOffer\": {\"availability\": \"available\"," + + "\"price\": {\"value\": {\"amount\": 10.0}}," + + "\"shippingOptions\": [{\"shippingCost\": {\"value\": {\"amount\": 2.5}}}], " + + "\"merchant\": {\"name\": \"Merchant A\"}}}"; + + AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + URL mockURL = mock(URL.class); + when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); + when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod(); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockConnection.getInputStream()).thenReturn(new ByteArrayInputStream(mockResponse.getBytes())); + when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); + + // Invoke the method + ProductDTO result = dataSource.getProductDTOById(demoProductId); + + // Verify the product DTO properties + assertEquals(demoProductId, result.getProductId()); + assertEquals("Amazon", result.getDataOrigin()); + assertEquals(true, result.isAvailable()); + assertEquals(10.0, result.getCurrentPrice()); + assertEquals(2.5, result.getDeliveryPrice()); + assertEquals("Merchant A", result.getMerchant()); + } + + @Test + void testGetProductDTOById_failedRequest() throws IOException { + // Set up the test environment + + AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + URL mockURL = mock(URL.class); + when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); + when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); + + // Invoke the method and verify the exception + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dataSource.getProductDTOById(demoProductId); + }); + + // Verify the exception message + assertEquals("Nothing found: Amazon API responded with error code 404", exception.getMessage()); + } + + @Test + void testGetProductDTOById_ioException() throws IOException { + // Set up the test environment + AmazonProductDataSource dataSource = mock(AmazonProductDataSource.class); + URL mockURL = mock(URL.class); + when(dataSource.createApiUrl(demoProductId)).thenReturn(mockURL); + when(dataSource.getProductDTOById(demoProductId)).thenCallRealMethod(); + when(dataSource.buildProductDTO(any(), anyString())).thenCallRealMethod(); + HttpURLConnection mockConnection = mock(HttpURLConnection.class); + when(mockURL.openConnection()).thenReturn(mockConnection); + when(mockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK); + when(mockConnection.getInputStream()).thenThrow(new IOException()); + + // Invoke the method and verify the exception + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + dataSource.getProductDTOById(demoProductId); + }); + + // Verify the exception message + assertEquals("Couldn't fulfill Amazon API request", exception.getMessage()); + } +} diff --git a/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java new file mode 100644 index 0000000..9d5917a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/api/dto/ProductDTOTest.java @@ -0,0 +1,33 @@ +package de.rwu.easydrop.api.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +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); + + String expectedString1 = "ProductDTO{12345 from Merchant A (Amazon) at 19,99 € (available: yes)}"; + String result1 = product1.toString(); + + assertEquals(expectedString1, result1); + } + + @Test + void testToString2() { + ProductDTO product2 = new ProductDTO("67890", "eBay"); + product2.setMerchant("Merchant B"); + product2.setCurrentPrice(9.99); + product2.setAvailable(false); + + String expectedString2 = "ProductDTO{67890 from Merchant B (eBay) at 9,99 € (available: no)}"; + String result2 = product2.toString(); + + assertEquals(expectedString2, result2); + } +} diff --git a/src/test/java/de/rwu/easydrop/demo/DemoClassTest.java b/src/test/java/de/rwu/easydrop/demo/DemoClassTest.java deleted file mode 100644 index fb4aa0e..0000000 --- a/src/test/java/de/rwu/easydrop/demo/DemoClassTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.rwu.easydrop.demo; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -public class DemoClassTest { - private final DemoClass demoClass = new DemoClass(); - - @Test - public void testInvertBoolTrue() { - assertEquals("Inverting true should return false", false, demoClass.invertBool(true)); - } - - @Test - public void testInvertBoolFalse() { - assertEquals("Inverting false should return true", true, demoClass.invertBool(false)); - } -} diff --git a/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java new file mode 100644 index 0000000..db7de79 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/util/ConfigImplTest.java @@ -0,0 +1,95 @@ +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 java.util.NoSuchElementException; + +import javax.naming.ConfigurationException; + +import org.junit.jupiter.api.BeforeEach; +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_KEY = "API_KEY"; + private final static String TESTDATA_VAL = "keyIsHere"; + + @BeforeEach + void setUp() { + config = Config.getInstance(); + } + + @Test + void testGetProperty_ExistingKey() throws ConfigurationException { + config.setConfigLocation(TESTDATA_PATH); + config.loadConfig(); + config.setProperty(TESTDATA_KEY, TESTDATA_VAL); + + String value = config.getProperty(TESTDATA_KEY); + + assertEquals(TESTDATA_VAL, value); + } + + @Test + void testGetProperty_ConfigNotLoaded() { + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> { + config.getProperty(TESTDATA_KEY); + }); + + assertEquals("Config has not been loaded", exception.getMessage()); + } + + @Test + void testGetProperty_NonExistingKey() { + try { + config.setConfigLocation(TESTDATA_PATH); + config.loadConfig(); + + NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> { + config.getProperty("I_DONT_EXIST"); + }); + + assertEquals("Requested config value does not exist", exception.getMessage()); + } catch (ConfigurationException e) { + fail("ConfigurationException should not be thrown"); + } + } + + @Test + void testSetProperty() { + try { + config.setConfigLocation(TESTDATA_PATH); + config.loadConfig(); + config.setProperty(TESTDATA_KEY, "12345"); + + assertEquals("12345", config.getProperty(TESTDATA_KEY)); + } catch (ConfigurationException e) { + fail("ConfigurationException should not be thrown"); + } + } + + @Test + void testLoadConfigSuccessfully() { + try { + config.setConfigLocation("testResources/testdata.properties"); + config.loadConfig(); + assertEquals(TESTDATA_VAL, config.getProperty(TESTDATA_KEY)); + } catch (ConfigurationException e) { + fail("ConfigurationException should not be thrown"); + } + } + + @Test + void testLoadConfigMissingFile() { + config.setConfigLocation("path/that/doesnt/exist/config.properties"); + + ConfigurationException exception = assertThrows(ConfigurationException.class, () -> { + config.loadConfig(); + }); + + assertEquals("Couldn't load required config file", exception.getMessage()); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/ConfigTest.java b/src/test/java/de/rwu/easydrop/util/ConfigTest.java new file mode 100644 index 0000000..8cf0c2a --- /dev/null +++ b/src/test/java/de/rwu/easydrop/util/ConfigTest.java @@ -0,0 +1,42 @@ +package de.rwu.easydrop.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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 ConfigTest { + private Config config; + + @BeforeEach + void setUp() throws ConfigurationException { + MockitoAnnotations.openMocks(this); + config = spy(Config.getInstance()); + } + + @Test + void testGetInstanceNull() { + config = null; + + Config newConfig = Config.getInstance(); + assertNotNull(newConfig); + } + + @Test + void testGetInstanceNotNull() { + Config newConfig = Config.getInstance(); + assertNotNull(newConfig); + } + + @Test + void testSetConfigLocation() { + String newPath = "new/location/config.properties"; + config.setConfigLocation(newPath); + assertEquals(newPath, config.getConfigLocation()); + } +} diff --git a/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java new file mode 100644 index 0000000..1c7d4f9 --- /dev/null +++ b/src/test/java/de/rwu/easydrop/util/FormattingUtilTest.java @@ -0,0 +1,58 @@ +package de.rwu.easydrop.util; + +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.Assertions; +import org.junit.jupiter.api.Test; + +class FormattingUtilTest { + + @Test + void testConstructorIsPrivate() + throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + // Check for private constructor + Constructor constructor = FormattingUtil.class.getDeclaredConstructor(); + assertTrue(Modifier.isPrivate(constructor.getModifiers())); + + // Make sure exception is thrown when instantiating + constructor.setAccessible(true); + assertThrows(InvocationTargetException.class, () -> { + constructor.newInstance(); + }); + } + + @Test + void testFormatEuro_positiveAmount() { + double amount = 1234.56; + String expectedFormattedAmount = "1.234,56 €"; + + String formattedAmount = FormattingUtil.formatEuro(amount); + + Assertions.assertEquals(expectedFormattedAmount, formattedAmount); + } + + @Test + void testFormatEuro_zeroAmount() { + double amount = 0.0; + String expectedFormattedAmount = "0,00 €"; + + String formattedAmount = FormattingUtil.formatEuro(amount); + + Assertions.assertEquals(expectedFormattedAmount, formattedAmount); + } + + @Test + void testFormatEuro_negativeAmount() { + double amount = -789.12; + String expectedFormattedAmount = "-789,12 €"; + + String formattedAmount = FormattingUtil.formatEuro(amount); + + Assertions.assertEquals(expectedFormattedAmount, formattedAmount); + } +} diff --git a/testResources/empty.properties b/testResources/empty.properties new file mode 100644 index 0000000..e69de29 diff --git a/testResources/testdata.properties b/testResources/testdata.properties new file mode 100644 index 0000000..cbf5875 --- /dev/null +++ b/testResources/testdata.properties @@ -0,0 +1 @@ +API_KEY=keyIsHere \ No newline at end of file