Merge branch 'dev' into 'main'

Release 0.2.0

See merge request team1/sandbox2!6
This commit is contained in:
Marvin Scham
2023-06-07 22:29:44 +00:00
82 changed files with 3028 additions and 374 deletions

2
.gitignore vendored
View File

@@ -3,6 +3,8 @@
###################################################################################################
config.properties
products-config.json
persistence.db
###################################################################################################
## Visual Studio Code #############################################################################

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,36 @@
# Changelog
## 0.2.0
### Added
- `EbayItemDataSource` (#37)
- Product data persistence via SQLite integration (#53)
- Mutation testing capabilities via PITest (#66)
- `Product` class
- Mapping between Product and its corresponding DTO
- Dependencies: Lombok for easier notation, JUnit 5 Params for repetitive tests
- Product validation, retrieval and persistence writing
- Product catalogue retrieval and persistence writing
- More unit tests
### Changed
- `AbstractDataSource` between DataSource interface and implementations to minimize code duplication
- Affects AmazonProductDataSource and related tests (~ #39)
- Testing resources moved to more intuitive location
- Demo `Main` method now uses proper Product retrieval
### Fixed
- Logback configuration is now linked correctly
- Config now properly adheres to singleton pattern
## 0.1.0
### Added
- (DataSource) → AmazonProductDataSource as first external data source (#39)
- ProductDTO as data holder structure for data from external sources (~ #39)
- (DataSource) → `AmazonProductDataSource` as first external data source (#39)
- `ProductDTO` as data holder structure for data from external sources (~ #39)
- Config for credential, base URLs etc. (~ #39)
- FormattingUtil for formatting price strings (~ #39)

View File

@@ -1,20 +0,0 @@
Dropshipping
Amazon vs. ebay
software schreiben die ermittelt, ob ein produkt bei amazon oder ebay günstiger bei einer der beiden ist und dann automatisch teurer beim anderen einstellen
ziel: keine lagerkosten, profit durch marge
problem: kann natürlich sein, dass es zwischenzeitlich beim einen händler nicht mehr verfügbar ist wenn der kunde es bei uns kaufen will,
sodass der automatische kauf bei der günstigeren plattform nicht mehr ausgelöst werden kann -->angebot muss also automatisch rausgenommen werden,
sobald produkt bei der günstigeren plattform nicht mehr verfügbar ist
- preise vergleichen automatisiert
- verfügbarkeit vergleichen automatisiert
- bspw. verkauf des günstigen produktes auf drittplattform (etsy) => Shopify?
welche anforderungen gibt es an mein produkt
- plattform/en: Amazon und eBay
- schnittstellen/APIs (zugang, kosten (pro aufruf))
- absicherung (fallback falls ebay/amazon nicht liefert -->alibaba)
- welche Art der Datenbank
- Lagerhaltung? =>eher nein
- algorithmus erstellen der soziale Medien nach Hype-Produkten durchsucht =>affiliate links?

View File

@@ -1,8 +0,0 @@
- Code should be free of faults
- Tests should be green by 90%
- Code is pushed to Team Repository
- Code has only few comments
- No commented out Code in Software
- Code is documented
- Code is executable on server
- Project is presented to customer / product owner / professor

BIN
Docs/DemoImage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,5 +0,0 @@
- Everything from code to comment is written in English; documentation may be in German
- Every developer has only one story in development. If the developer needs to halt with development in their story due to unforeseen reasons, they may already start with another story.
- Every branch works on one story
- User stories should be written in a clear way so that every developer can understand in what development status the ticket is
- Another rule

View File

@@ -1,5 +0,0 @@
- Welche Datenbank? AWS ja nein? =>Friedrich fragen (Rocketchat)
- Welche Werkzeuge? VSC
- Sprache: Java
- Kein Frontend
-

105
README.md
View File

@@ -1,92 +1,43 @@
# Sandbox2
# EasyDrop
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin http://gitlab.fbe-adswen.rwu.de/team1/sandbox2.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
- [ ] [Set up project integrations](http://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
![Version](https://img.shields.io/badge/version-0.2.0-blue)
![Pipeline](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/badges/main/pipeline.svg)
[![Coverage](https://sonar.fbe-adswen.rwu.de/api/project_badges/measure?project=de.rwu%3Aeasydrop&metric=coverage&token=sqb_2fe80aed361468170aaef32a0ff96d596456cdd1)](https://sonar.fbe-adswen.rwu.de/dashboard?id=de.rwu%3Aeasydrop)
![JAMANN](https://img.shields.io/badge/Auszahlung-Letzte%20Woche-brightgreen)
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
Get rich quick™ with fully automated dropshipping!
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Visuals
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
![Demo Image](Docs/DemoImage.png)
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Installation ⚙
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
As the application is still in development, development software will be required along its regular execution environment.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
- JDK 17
- Maven
- _[More/context](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/wikis/Projektdokumentation/Development#software)_
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
## Usage 🛠
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
Set up the required configuration files, use the corresponding demo files for orientation.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
- `config/config.properties` for API authorization
- `products-config.json` to define product catalogues to use for dropshipping
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
Run the following command to execute the program:
## License
For open source projects, say how it is licensed.
```sh
mvn compile exec:java -Dexec.mainClass="de.rwu.easydrop.Main"
```
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
## Roadmap 🏁
_Future ideas!_
## Contributing 👷‍♂️👷‍♀️
Contribution guidelines are available in the [project wiki](https://gitlab.fbe-adswen.rwu.de/team1/sandbox2/-/wikis/Richtlinien/Development)

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,2 @@
mvn test-compile org.pitest:pitest-maven:mutationCoverage
start target/pit-reports/index.html

View File

@@ -0,0 +1,7 @@
mvn clean verify sonar:sonar -Pcoverage \
-Dsonar.projectKey=EasyDrop \
-Dsonar.projectName='EasyDrop' \
-Dsonar.host.url=http://localhost:9000 \
-Dsonar.token=sqp_82d35689c620c15fd1064549375e17a2a5b0b931
start http://localhost:9000/dashboard?id=EasyDrop

View File

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

View File

@@ -0,0 +1,16 @@
{
"products": [
{
"name": "Gigabyte GeForce RTX 3060",
"description": "Very epic GPU",
"identifiers": [
{
"Amazon": "B096Y2TYKV"
},
{
"eBay": "Gigabyte GeForce RTX 3060"
}
]
}
]
}

2
lombok.config Normal file
View File

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

37
pom.xml
View File

@@ -5,7 +5,7 @@
<groupId>de.rwu</groupId>
<artifactId>easydrop</artifactId>
<packaging>jar</packaging>
<version>0.1.0</version>
<version>0.2.0</version>
<name>EasyDrop</name>
<url>http://maven.apache.org</url>
@@ -16,16 +16,29 @@
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.0</version>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<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>
</dependency>
@@ -47,6 +60,12 @@
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.pitest</groupId>
<artifactId>pitest-junit5-plugin</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
@@ -57,6 +76,12 @@
<artifactId>logback-classic</artifactId>
<version>1.4.7</version>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.42.0.0</version>
</dependency>
</dependencies>
<build>
@@ -88,6 +113,12 @@
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
</plugin>
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.14.1</version>
</plugin>
</plugins>
</build>

View File

@@ -1,12 +1,22 @@
package de.rwu.easydrop;
import java.util.List;
import javax.naming.ConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sqlite.SQLiteDataSource;
import de.rwu.easydrop.api.client.AmazonProductDataSource;
import de.rwu.easydrop.api.client.DataSourceFactory;
import de.rwu.easydrop.data.connector.AbstractProductPersistence;
import de.rwu.easydrop.data.connector.SQLiteConnector;
import de.rwu.easydrop.model.ProductCatalogue;
import de.rwu.easydrop.service.retriever.CatalogueRetriever;
import de.rwu.easydrop.service.retriever.ProductRetriever;
import de.rwu.easydrop.service.writer.CatalogueWriter;
import de.rwu.easydrop.util.Config;
import de.rwu.easydrop.util.ProductsConfig;
/**
* Kickoff point for the service.
@@ -33,17 +43,20 @@ public final class Main {
*/
public static void main(final String[] args) throws ConfigurationException {
Config config = Config.getInstance();
config.loadConfig();
String amznBaseUrl = config.getProperty("AMAZON_API_URL");
String amznApiKey = config.getProperty("AMAZON_API_KEY");
String testProduct = null;
ProductsConfig pConfig = ProductsConfig.getInstance();
DataSourceFactory dataSourceFactory = new DataSourceFactory(config);
ProductRetriever retriever = new ProductRetriever(dataSourceFactory);
CatalogueRetriever catRetriever = new CatalogueRetriever(pConfig, retriever);
AbstractProductPersistence db = new SQLiteConnector(new SQLiteDataSource());
CatalogueWriter catWriter = new CatalogueWriter(db);
AmazonProductDataSource amznSrc = new AmazonProductDataSource(amznBaseUrl, amznApiKey);
try {
testProduct = amznSrc.getProductDTOById("B096Y2TYKV").toString();
LOGGER.info(testProduct);
} catch (IllegalArgumentException e) {
LOGGER.error("Something went wrong :(", e);
catRetriever.loadCatalogues();
List<ProductCatalogue> pCats = catRetriever.getProductCatalogues();
catWriter.writeCatalogues(pCats);
for (ProductCatalogue pCat : pCats) {
String pCatStr = pCat.toString();
LOGGER.info(pCatStr);
}
}
}

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;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
@@ -18,7 +14,7 @@ import de.rwu.easydrop.api.dto.ProductDTO;
*
* @since 0.1.0
*/
public final class AmazonProductDataSource implements DataSource {
public final class AmazonProductDataSource extends AbstractDataSource {
/**
* Name of this data source.
*/
@@ -28,7 +24,7 @@ public final class AmazonProductDataSource implements DataSource {
*/
private String baseUrl;
/**
* Credential key to authorize acccess.
* Credential key to authorize access.
*/
private String apiKey;
/**
@@ -52,48 +48,7 @@ public final class AmazonProductDataSource implements DataSource {
}
@Override
public ProductDTO getProductDTOById(final String productId) throws IllegalArgumentException {
StringBuilder response = new StringBuilder();
ProductDTO product = new ProductDTO(productId, DATA_ORIGIN);
try {
URL apiUrl = createApiUrl(productId);
HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Credential", apiKey);
int responseCode = connection.getResponseCode();
BufferedReader reader;
if (responseCode == HttpURLConnection.HTTP_OK) {
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} else {
throw new IllegalArgumentException(
"Nothing found: Amazon API responded with error code " + responseCode);
}
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();
buildProductDTO(product, response.toString());
} catch (IOException e) {
throw new IllegalArgumentException("Couldn't fulfill Amazon API request");
}
return product;
}
/**
* Enriches a ProductDTO with API-gathered data.
*
* @param product Unfinished ProductDTO
* @param json Product data
* @return Finished ProductDTO
*/
public ProductDTO buildProductDTO(final ProductDTO product, final String json) {
protected ProductDTO buildProductDTO(final ProductDTO product, final String json) {
String root = "$.featuredOffer.";
ReadContext ctx = JsonPath.parse(json);
@@ -112,8 +67,11 @@ public final class AmazonProductDataSource implements DataSource {
return product;
}
/**
* @param productId ASIN
*/
@Override
public URL createApiUrl(final String productId) throws MalformedURLException {
protected URL createApiUrl(final String productId) throws MalformedURLException {
return new URL(baseUrl
+ "/products/2020-08-26/products/"
+ productId
@@ -122,4 +80,14 @@ public final class AmazonProductDataSource implements DataSource {
+ "&locale="
+ LOCALE);
}
@Override
protected String getDataOrigin() {
return DATA_ORIGIN;
}
@Override
protected String getApiKey() {
return this.apiKey;
}
}

View File

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

View File

@@ -0,0 +1,81 @@
package de.rwu.easydrop.api.client;
import javax.naming.ConfigurationException;
import de.rwu.easydrop.data.connector.AbstractProductPersistence;
import de.rwu.easydrop.exception.PersistenceException;
import de.rwu.easydrop.util.Config;
/**
* Factory for Data Sources.
*
* @since 0.2.0
*/
public class DataSourceFactory {
/**
* The data source config.
*/
private Config config;
/**
* Persistence interface.
*/
private AbstractProductPersistence persistence = null;
/**
* @param newPersistence the persistence to set
*/
public void setPersistence(final AbstractProductPersistence newPersistence) {
this.persistence = newPersistence;
}
/**
* @param newConfig the config to set
*/
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);
}
/**
* Creates a persistence data source.
*
* @return ProductPersistenceInterface
*/
public AbstractProductPersistence createProductPersistenceDataSource() {
if (persistence == null) {
throw new PersistenceException("Persistence is not set");
}
return persistence;
}
}

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

View File

@@ -0,0 +1,34 @@
package de.rwu.easydrop.data.connector;
import de.rwu.easydrop.api.client.AbstractDataSource;
import de.rwu.easydrop.api.dto.ProductDTO;
/**
* Allows connecting to a persistent product data store.
*
* @since 0.2.0
*/
public abstract class AbstractProductPersistence extends AbstractDataSource {
/**
* Data origin.
*/
public static final String DATA_ORIGIN = "Persistence";
/**
* Writes a ProductDTO to persistence.
*
* @param dto
*/
public abstract void saveProduct(ProductDTO dto);
/**
* Gets a ProductDTO from persistence.
*/
@Override
public abstract ProductDTO getProductDTOById(String productId);
/**
* Deletes all data from persistence.
*/
public abstract void clearData();
}

View File

@@ -1,10 +0,0 @@
package de.rwu.easydrop.data.connector;
/**
* Allows connecting to a SQLite Database.
*
* TODO implement
*/
public class DatabaseConnector {
}

View File

@@ -0,0 +1,173 @@
package de.rwu.easydrop.data.connector;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.sqlite.SQLiteDataSource;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.exception.PersistenceException;
/**
* Allows connecting to a SQLite Database.
*
* @since 0.2.0
*/
public final class SQLiteConnector extends AbstractProductPersistence {
/**
* Data origin.
*/
private static final String DATA_ORIGIN = "SQLite";
/**
* SQLite Database.
*/
private SQLiteDataSource db;
/**
* @param src the db to set
*/
public void setDb(final SQLiteDataSource src) {
this.db = src;
}
/**
* Path to SQLite db file.
*/
private static final String PERSISTENCE_PATH = "jdbc:sqlite:persistence.db";
/**
* Creates instance.
*
* @param src SQLite Data Source
*/
public SQLiteConnector(final SQLiteDataSource src) {
db = src;
db.setUrl(PERSISTENCE_PATH);
initializeDatabase();
}
private void initializeDatabase() {
try {
// Create a new database connection
Connection connection = db.getConnection();
// Execute SQL statements to create tables
Statement statement = connection.createStatement();
statement.execute(
"CREATE TABLE IF NOT EXISTS products ("
+ "dataOrigin TEXT, "
+ "productId TEXT, "
+ "currentPrice REAL, "
+ "merchant TEXT, "
+ "deliveryPrice REAL, "
+ "available INT, "
+ "lastupdate TEXT, "
+ "UNIQUE(productId, dataOrigin) ON CONFLICT REPLACE"
+ ")");
// Close the statement and connection
statement.close();
connection.close();
} catch (SQLException e) {
throw new PersistenceException("Something went wrong while initializing SQLite DB", e);
}
}
/**
* Writes a ProductDTO to persistence.
*
* @param dto
*/
public void saveProduct(final ProductDTO dto) {
String query = "INSERT INTO products ("
+ "dataOrigin, productId, currentPrice, merchant, "
+ "deliveryPrice, available, lastupdate"
+ ") VALUES ("
+ "?, ?, ?, ?, ?, ?, datetime('now', 'localtime')"
+ ")";
try (Connection connection = db.getConnection();
PreparedStatement statement = connection.prepareStatement(query)) {
int index = 0;
statement.setString(++index, dto.getDataOrigin());
statement.setString(++index, dto.getProductId());
statement.setDouble(++index, dto.getCurrentPrice());
statement.setString(++index, dto.getMerchant());
statement.setDouble(++index, dto.getDeliveryPrice());
statement.setBoolean(++index, dto.isAvailable());
statement.executeUpdate();
} catch (SQLException e) {
throw new PersistenceException("Something went wrong while saving to SQLite", e);
}
}
@Override
public ProductDTO getProductDTOById(final String productId) {
String query = "SELECT * FROM products WHERE productId = ?";
ProductDTO dto = null;
try (Connection connection = db.getConnection();
PreparedStatement statement = connection.prepareStatement(query)) {
statement.setString(1, productId);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
dto = new ProductDTO(resultSet.getString("productId"),
resultSet.getString("dataOrigin"));
dto.setCurrentPrice(resultSet.getDouble("currentPrice"));
dto.setMerchant(resultSet.getString("merchant"));
dto.setDeliveryPrice(resultSet.getDouble("deliveryPrice"));
dto.setAvailable(resultSet.getBoolean("available"));
}
}
} catch (SQLException e) {
throw new PersistenceException("Something went wrong while reading from SQLite", e);
}
return dto;
}
/**
* Deletes all data from persistence.
*/
public void clearData() {
try (Connection connection = db.getConnection();
Statement statement = connection.createStatement()) {
String query = "DELETE FROM products";
statement.executeUpdate(query);
} catch (SQLException e) {
throw new PersistenceException("Something went wrong while clearing the database", e);
}
}
@Override
protected String getDataOrigin() {
return DATA_ORIGIN;
}
@Override
protected String getApiKey() {
throw new UnsupportedOperationException(
this.getClass().getName() + " doesn't support getApiKey");
}
@Override
protected ProductDTO buildProductDTO(final ProductDTO product, final String json) {
throw new UnsupportedOperationException(
this.getClass().getName() + " doesn't support buildProductDTO");
}
@Override
protected URL createApiUrl(final String productIdentifier) {
throw new UnsupportedOperationException(
this.getClass().getName() + " doesn't support createApiUrl");
}
}

View File

@@ -1,6 +1,6 @@
/**
* Connectors for databases.
*
* TODO implement
* @since 0.2.0
*/
package de.rwu.easydrop.data.connector;

View File

@@ -1,10 +0,0 @@
package de.rwu.easydrop.data.dao;
/**
* Product data access object.
*
* TODO implement
*/
public class ProductDAO {
}

View File

@@ -1,6 +0,0 @@
/**
* Data access objects for business objects created from persistence.
*
* TODO implement
*/
package de.rwu.easydrop.data.dao;

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

@@ -1,6 +1,6 @@
/**
* Structure for business objects and persisting their info.
*
* TODO implement
* @since 0.2.0
*/
package de.rwu.easydrop.data;

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,27 @@
package de.rwu.easydrop.exception;
/**
* Exception that signifies a problem with data persistence.
*
* @since 0.2.0
*/
public class PersistenceException extends RuntimeException {
/**
* Throws an exception that signifies the data of a Product are invalid.
*
* @param message
*/
public PersistenceException(final String message) {
super(message);
}
/**
* Throws an exception that signifies the data of a Product are invalid.
*
* @param message
* @param cause
*/
public PersistenceException(final String message, final Throwable cause) {
super(message, cause);
}
}

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,79 @@
package de.rwu.easydrop.model;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* Holds Product instances for one product from different data sources.
*
* @since 0.2.0
*/
@Data
public class ProductCatalogue {
/**
* Product name.
*/
private String productName;
/**
* Product description.
*/
private String description;
/**
* Product collection.
*/
private List<Product> products;
/**
* Creates new Product Catalogue.
*
* @param newProductName
* @param newDescription
*/
public ProductCatalogue(final String newProductName, final String newDescription) {
this.productName = newProductName;
this.description = newDescription;
this.products = new ArrayList<>();
}
/**
* Adds a product to the catalogue.
*
* @param product
*/
public void addProduct(final Product product) {
products.add(product);
}
/**
* Removes a product from the catalogue.
*
* @param product
*/
public void removeProduct(final Product product) {
products.remove(product);
}
/**
* Removes all products from the catalogue.
*/
public void clearProducts() {
products = new ArrayList<>();
}
/**
* Outputs the catalogue as a string.
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Product Catalogue: ").append(productName).append("\n");
sb.append("Description: ").append(description).append("\n");
sb.append("Products:\n");
for (Product product : products) {
sb.append(product.toString()).append("\n");
}
return sb.toString();
}
}

View File

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

View File

@@ -1,14 +1,62 @@
package de.rwu.easydrop.service.mapping;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.model.Product;
/**
* Maps between Product, ProductDAO and ProductDTO.
* Maps between Product, ProductDTO and ProductDTO.
*
* TODO implement
* @since 0.2.0
*
* @see Product
* @see ProductDTO
* @see ProductDAO
* @see ProductDTO
*/
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;
}
/**
* Creates a ProductDTO object from a corresponding Product.
*
* @param product Product
* @return ProductDTO
*/
public static ProductDTO mapProductToDTO(final Product product) {
ProductDTO dto = new ProductDTO(product.getProductId(), product.getDataOrigin());
dto.setAvailable(product.isAvailable());
dto.setCurrentPrice(product.getCurrentPrice());
dto.setDeliveryPrice(product.getDeliveryPrice());
dto.setMerchant(product.getMerchant());
return dto;
}
}

View File

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

View File

@@ -1,6 +1,6 @@
/**
* Packages for supporting business logic.
*
* TODO implement
* @since 0.2.0
*/
package de.rwu.easydrop.service;

View File

@@ -1,6 +1,6 @@
/**
* Supports diverse business processes and enforces business rules.
*
* TODO implement
* @since 0.2.0
*/
package de.rwu.easydrop.service.processing;

View File

@@ -0,0 +1,78 @@
package de.rwu.easydrop.service.retriever;
import java.util.ArrayList;
import java.util.List;
import javax.naming.ConfigurationException;
import de.rwu.easydrop.exception.InvalidProductException;
import de.rwu.easydrop.model.Product;
import de.rwu.easydrop.model.ProductCatalogue;
import de.rwu.easydrop.util.ProductsConfig;
import lombok.Data;
/**
* Retrieves data for all products of multiple catalogues.
*
* @since 0.2.0
*/
@Data
public class CatalogueRetriever {
/**
* User-configured products.
*/
private ProductsConfig productsConfig;
/**
* Product Retriever.
*/
private ProductRetriever productRetriever;
/**
* Product catalogue.
*/
private List<ProductCatalogue> productCatalogues;
/**
* Creates a new instance.
*
* @param newProductsConfig
* @param newProductRetriever
*/
public CatalogueRetriever(
final ProductsConfig newProductsConfig, final ProductRetriever newProductRetriever) {
productRetriever = newProductRetriever;
productsConfig = newProductsConfig;
productCatalogues = new ArrayList<>();
}
/**
* Loads catalogues as configured by the user.
*
* @throws ConfigurationException
*/
public void loadCatalogues() throws ConfigurationException {
productsConfig.loadConfig();
for (ProductCatalogue pCat : productsConfig.getProductCatalogues()) {
ProductCatalogue newProductCatalogue = new ProductCatalogue(
pCat.getProductName(), pCat.getDescription());
for (Product product : pCat.getProducts()) {
Product newProduct = new Product();
newProduct.setDataOrigin(product.getDataOrigin());
newProduct.setProductId(product.getProductId());
if (newProduct.getDataOrigin().equals("Amazon")) {
newProduct = productRetriever.getProductFromAmazon(product.getProductId());
} else if (newProduct.getDataOrigin().equals("eBay")) {
newProduct = productRetriever.getProductFromEbay(product.getProductId());
} else {
throw new InvalidProductException("Product data origin is invalid");
}
newProductCatalogue.addProduct(newProduct);
}
productCatalogues.add(newProductCatalogue);
}
}
}

View File

@@ -0,0 +1,84 @@
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.data.connector.AbstractProductPersistence;
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;
}
/**
* Retrieves a product from persistence.
*
* @param productId
* @return Product from persistence
*/
public Product getProductFromPersistence(final String productId) {
AbstractProductPersistence src = dataSourceFactory.createProductPersistenceDataSource();
ProductDTO dto = src.getProductDTOById(productId);
Product product = ProductMapper.mapProductFromDTO(dto);
ProductValidator.validate(product);
return product;
}
}

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;
import java.util.HashSet;
import java.util.Set;
import de.rwu.easydrop.exception.InvalidProductException;
import de.rwu.easydrop.model.Product;
/**
* Confirms validity of Product data.
*
* @since 0.1.0
* @since 0.2.0
*/
public class ProductValidator {
public final class ProductValidator {
/**
* Private constructor to prevent unwanted instantiation.
*
* @throws UnsupportedOperationException always
*/
private ProductValidator() throws UnsupportedOperationException {
throw new UnsupportedOperationException("This is a validator class, don't instantiate it.");
}
/**
* Makes sure a Product does not contain invalid information.
*
* @param product the Product
*/
public static void validate(final Product product) {
if (product.getCurrentPrice() == 0.00) {
throw new InvalidProductException("Current price cannot be 0.00");
}
if (!isInValidDataOrigins(product.getDataOrigin())) {
throw new InvalidProductException("Unknown data source");
}
if (product.getProductId().equals("")) {
throw new InvalidProductException("Product ID cannot be empty");
}
}
/**
* Checks whether a dataOrigin is within the set of valid ones.
*
* @param dataOrigin like "Amazon"
* @return true if valid
*/
public static boolean isInValidDataOrigins(final String dataOrigin) {
Set<String> validOrigins = new HashSet<>();
validOrigins.add("Amazon");
validOrigins.add("eBay");
return validOrigins.contains(dataOrigin);
}
}

View File

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

View File

@@ -0,0 +1,47 @@
package de.rwu.easydrop.service.writer;
import java.util.List;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.data.connector.AbstractProductPersistence;
import de.rwu.easydrop.model.Product;
import de.rwu.easydrop.model.ProductCatalogue;
import de.rwu.easydrop.service.mapping.ProductMapper;
import de.rwu.easydrop.service.validation.ProductValidator;
/**
* Writes data for all products of multiple catalogues to persistence.
*
* @since 0.2.0
*/
public final class CatalogueWriter {
/**
* Holds a persistence reference.
*/
private AbstractProductPersistence persistence;
/**
* Creates new instance.
*
* @param newPersistence
*/
public CatalogueWriter(final AbstractProductPersistence newPersistence) {
persistence = newPersistence;
}
/**
* Writes all products of specified catalogues to persistence.
*
* @param catalogues
*/
public void writeCatalogues(final List<ProductCatalogue> catalogues) {
for (ProductCatalogue pCat : catalogues) {
for (Product product : pCat.getProducts()) {
ProductValidator.validate(product);
ProductDTO dto = ProductMapper.mapProductToDTO(product);
persistence.saveProduct(dto);
}
}
}
}

View File

@@ -0,0 +1,38 @@
package de.rwu.easydrop.service.writer;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.data.connector.AbstractProductPersistence;
import de.rwu.easydrop.model.Product;
import de.rwu.easydrop.service.mapping.ProductMapper;
import de.rwu.easydrop.service.validation.ProductValidator;
/**
* Wrapper for writing product info to persistence.
*
* @since 0.2.0
*/
public class ProductWriter {
/**
* Persistence.
*/
private AbstractProductPersistence persistence;
/**
* @param newPersistence the persistence to set
*/
public void setPersistence(final AbstractProductPersistence newPersistence) {
this.persistence = newPersistence;
}
/**
* Validates and saves product to persistence.
*
* @param product
*/
public void writeProductToPersistence(final Product product) {
ProductValidator.validate(product);
ProductDTO dto = ProductMapper.mapProductToDTO(product);
persistence.saveProduct(dto);
}
}

View File

@@ -0,0 +1,6 @@
/**
* Writes Objects to a data store.
*
* @since 0.2.0
*/
package de.rwu.easydrop.service.writer;

View File

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

View File

@@ -27,4 +27,14 @@ public final class FormattingUtil {
public static String formatEuro(final double amount) {
return String.format(Locale.GERMAN, "%,.2f", amount) + "";
}
/**
* Makes a string URL ready. For now, only spaces are replaced.
*
* @param str
* @return URL-ready string
*/
public static String urlEncode(final String str) {
return str.replace(" ", "+");
}
}

View File

@@ -0,0 +1,142 @@
package de.rwu.easydrop.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.naming.ConfigurationException;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.JsonPathException;
import de.rwu.easydrop.model.Product;
import de.rwu.easydrop.model.ProductCatalogue;
/**
* Reads the user-specified catalogue of products.
*
* @since 0.2.0
*/
public final class ProductsConfig {
/**
* Products config location.
*/
private String configLocation = "config/products-config.json";
/**
* @return the configLocation
*/
public String getConfigLocation() {
return configLocation;
}
/**
* @param newConfigLocation the configLocation to set
*/
public void setConfigLocation(final String newConfigLocation) {
this.configLocation = newConfigLocation;
}
/**
* Holds the configured products.
*/
private List<ProductCatalogue> productCatalogues;
/**
* @return the product catalogues
*/
public List<ProductCatalogue> getProductCatalogues() {
return productCatalogues;
}
/**
* Singleton instance.
*/
private static ProductsConfig instance = null;
/**
* Private constructor to prevent external instantiation.
*/
private ProductsConfig() {
productCatalogues = new ArrayList<>();
}
/**
* Returns current products config instance.
*
* @return Products Config instance
*/
public static ProductsConfig getInstance() {
if (instance == null) {
instance = new ProductsConfig();
}
return instance;
}
/**
* Loads user-specified configuration into productCatalogues attribute.
*
* @throws ConfigurationException
*/
public void loadConfig() throws ConfigurationException {
try {
File jsonFile = new File(configLocation).getAbsoluteFile();
ArrayList<HashMap<String, Object>> jsonProducts = JsonPath.read(jsonFile, "$.products");
setProductCatalogues(jsonProducts);
} catch (IOException e) {
throw new ConfigurationException("Couldn't load required products config file");
} catch (JsonPathException e) {
throw new ConfigurationException("Products config is empty or malformed");
}
}
/**
* Loads catalogues from JSON.
*
* @param jsonCatalogues
*/
private void setProductCatalogues(final ArrayList<HashMap<String, Object>> jsonCatalogues) {
for (HashMap<String, Object> productCatalogue : jsonCatalogues) {
String name = productCatalogue.get("name").toString();
String desc = productCatalogue.get("description").toString();
String identifiers = productCatalogue.get("identifiers").toString();
ProductCatalogue pCat = new ProductCatalogue(name, desc);
addProductsToCatalogue(identifiers, pCat);
productCatalogues.add(pCat);
}
}
/**
* Loads products from JSON.
*
* @param jsonIdentifiers
* @param pCat
*/
private void addProductsToCatalogue(final String jsonIdentifiers, final ProductCatalogue pCat) {
ArrayList<HashMap<String, Object>> identifiers = JsonPath.read(jsonIdentifiers, "$");
for (HashMap<String, Object> product : identifiers) {
String dataOrigin = product.keySet().iterator().next();
String identifier = product.get(dataOrigin).toString();
Product newProduct = new Product();
newProduct.setDataOrigin(dataOrigin);
newProduct.setProductId(identifier);
pCat.addProduct(newProduct);
}
}
/**
* Resets the contained product catalogues.
*/
public void reset() {
productCatalogues = new ArrayList<>();
}
}

View File

@@ -1,6 +1,6 @@
/**
* General utility such as formatting helpers.
*
* TODO implement
* @since 0.1.0
*/
package de.rwu.easydrop.util;

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
package de.rwu.easydrop.api.client;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import javax.naming.ConfigurationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.sqlite.SQLiteDataSource;
import de.rwu.easydrop.data.connector.SQLiteConnector;
import de.rwu.easydrop.exception.PersistenceException;
import de.rwu.easydrop.util.Config;
class DataSourceFactoryTest {
@Mock
private Config config;
private DataSourceFactory dataSourceFactory;
@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());
}
@Test
void createProductPersistenceDataSource_NullPersistence() {
PersistenceException exception = assertThrows(PersistenceException.class, () -> {
dataSourceFactory.createProductPersistenceDataSource();
});
assertEquals("Persistence is not set", exception.getMessage());
}
@Test
void createProductPersistenceDataSource_WorkingPersistence() {
dataSourceFactory.setPersistence(new SQLiteConnector(new SQLiteDataSource()));
assertDoesNotThrow(() -> dataSourceFactory.createProductPersistenceDataSource());
}
}

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;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.Test;
class ProductDTOTest {
@Test
void testToString1() {
ProductDTO product1 = new ProductDTO("12345", "Amazon");
product1.setMerchant("Merchant A");
product1.setCurrentPrice(19.99);
product1.setAvailable(true);
void constructor_SetsProductIdAndDataOrigin() {
// Arrange
String productId = "12345";
String dataOrigin = "Amazon";
String expectedString1 = "ProductDTO{12345 from Merchant A (Amazon) at 19,99 € (available: yes)}";
String result1 = product1.toString();
// Act
ProductDTO productDTO = new ProductDTO(productId, dataOrigin);
assertEquals(expectedString1, result1);
// Assert
assertEquals(productId, productDTO.getProductId());
assertEquals(dataOrigin, productDTO.getDataOrigin());
}
@Test
void testToString2() {
ProductDTO product2 = new ProductDTO("67890", "eBay");
product2.setMerchant("Merchant B");
product2.setCurrentPrice(9.99);
product2.setAvailable(false);
void gettersAndSetters_WorkAsExpected() {
// Arrange
ProductDTO productDTO = new ProductDTO("12345", "Amazon");
String expectedString2 = "ProductDTO{67890 from Merchant B (eBay) at 9,99 € (available: no)}";
String result2 = product2.toString();
// Act and Assert
assertEquals("12345", productDTO.getProductId());
assertEquals("Amazon", productDTO.getDataOrigin());
assertEquals(expectedString2, result2);
// Modify fields
productDTO.setProductId("54321");
productDTO.setDataOrigin("eBay");
// Assert
assertEquals("54321", productDTO.getProductId());
assertEquals("eBay", productDTO.getDataOrigin());
}
@Test
void defaultConstructor_SetsDefaultValues() {
// Act
ProductDTO productDTO = new ProductDTO(null, null);
// Assert
assertNull(productDTO.getProductId());
assertNull(productDTO.getDataOrigin());
assertEquals(0.0, productDTO.getCurrentPrice());
assertNull(productDTO.getMerchant());
assertEquals(0.0, productDTO.getDeliveryPrice());
assertFalse(productDTO.isAvailable());
}
}

View File

@@ -0,0 +1,195 @@
package de.rwu.easydrop.data.connector;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.doThrow;
import java.sql.SQLException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.sqlite.SQLiteDataSource;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.exception.PersistenceException;
@TestInstance(Lifecycle.PER_CLASS)
class SQLiteConnectorTest {
private static final String TEST_PRODUCT_ID = "12345";
private SQLiteConnector sqliteConnector;
@Mock
private SQLiteDataSource mockDataSource;
@BeforeAll
public void setup() {
sqliteConnector = new SQLiteConnector(new SQLiteDataSource());
}
@BeforeEach
public void clearDatabase() {
MockitoAnnotations.openMocks(this);
}
@Test
void saveProduct_ValidProduct_SuccessfullySaved() {
// Arrange
sqliteConnector.clearData();
ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon");
ProductDTO.setDataOrigin("Amazon");
ProductDTO.setProductId(TEST_PRODUCT_ID);
ProductDTO.setCurrentPrice(9.99);
ProductDTO.setMerchant("Sample Merchant");
ProductDTO.setDeliveryPrice(2.50);
ProductDTO.setAvailable(true);
// Act
assertDoesNotThrow(() -> sqliteConnector.saveProduct(ProductDTO));
// Assert
ProductDTO savedProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID);
assertNotNull(savedProductDTO);
assertEquals("Amazon", savedProductDTO.getDataOrigin());
assertEquals(TEST_PRODUCT_ID, savedProductDTO.getProductId());
assertEquals(9.99, savedProductDTO.getCurrentPrice());
assertEquals("Sample Merchant", savedProductDTO.getMerchant());
assertEquals(2.50, savedProductDTO.getDeliveryPrice());
assertTrue(savedProductDTO.isAvailable());
}
@Test
void getProductDTOById_ProductExists_ReturnsProductDTO() {
// Arrange
sqliteConnector.clearData();
insertSampleProduct();
// Act
ProductDTO ProductDTO = sqliteConnector.getProductDTOById(TEST_PRODUCT_ID);
// Assert
assertNotNull(ProductDTO);
assertEquals("Amazon", ProductDTO.getDataOrigin());
assertEquals(TEST_PRODUCT_ID, ProductDTO.getProductId());
assertEquals(9.99, ProductDTO.getCurrentPrice());
assertEquals("Sample Merchant", ProductDTO.getMerchant());
assertEquals(2.50, ProductDTO.getDeliveryPrice());
assertTrue(ProductDTO.isAvailable());
}
@Test
void constructor_ThrowsPersistenceException_OnSQLException() {
try {
doThrow(SQLException.class).when(mockDataSource).getConnection();
PersistenceException exception = assertThrows(PersistenceException.class, () -> {
new SQLiteConnector(mockDataSource);
});
assertEquals("Something went wrong while initializing SQLite DB", exception.getMessage());
} catch (SQLException e) {
fail("No SQLException should be thrown");
}
}
@Test
void getProductDTOById_ProductDoesNotExist_ReturnsNull() {
// Act
ProductDTO ProductDTO = sqliteConnector.getProductDTOById("FAKE_ID");
// Assert
assertNull(ProductDTO);
}
@Test
void saveProduct_ThrowsPersistenceException_OnSQLException() throws SQLException {
// Arrange
ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon");
sqliteConnector.setDb(mockDataSource);
doThrow(SQLException.class).when(mockDataSource).getConnection();
// Act and Assert
assertThrows(PersistenceException.class, () -> sqliteConnector.saveProduct(ProductDTO));
}
@Test
void getProductDTOById_ThrowsPersistenceException_OnSQLException() throws SQLException {
// Arrange
String productId = "12345";
sqliteConnector.setDb(mockDataSource);
doThrow(SQLException.class).when(mockDataSource).getConnection();
// Act and Assert
assertThrows(PersistenceException.class, () -> sqliteConnector.getProductDTOById(productId));
}
@Test
void clearData_ThrowsPersistenceException_OnSQLException() throws SQLException {
// Arrange
sqliteConnector.setDb(mockDataSource);
doThrow(SQLException.class).when(mockDataSource).getConnection();
// Act and Assert
assertThrows(PersistenceException.class, () -> sqliteConnector.clearData());
}
private void insertSampleProduct() {
ProductDTO ProductDTO = new ProductDTO(TEST_PRODUCT_ID, "Amazon");
ProductDTO.setCurrentPrice(9.99);
ProductDTO.setMerchant("Sample Merchant");
ProductDTO.setDeliveryPrice(2.50);
ProductDTO.setAvailable(true);
sqliteConnector.saveProduct(ProductDTO);
}
@Test
void getDataOrigin_ReturnsCorrectDataOrigin() {
// Arrange
SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource());
// Act
String dataOrigin = connector.getDataOrigin();
// Assert
assertEquals("SQLite", dataOrigin);
}
@Test
void getApiKey_UnsupportedOperationExceptionThrown() {
// Arrange
SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource());
// Act and Assert
assertThrows(UnsupportedOperationException.class, connector::getApiKey);
}
@Test
void buildProductDTO_UnsupportedOperationExceptionThrown() {
// Arrange
SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource());
ProductDTO product = new ProductDTO("ASIN123", "Amazon");
String json = "{\"productId\":\"ASIN123\",\"dataOrigin\":\"Amazon\"}";
// Act and Assert
assertThrows(UnsupportedOperationException.class, () -> connector.buildProductDTO(product, json));
}
@Test
void createApiUrl_UnsupportedOperationExceptionThrown() {
// Arrange
SQLiteConnector connector = new SQLiteConnector(new SQLiteDataSource());
String productIdentifier = "ASIN123";
// Act and Assert
assertThrows(UnsupportedOperationException.class, () -> connector.createApiUrl(productIdentifier));
}
}

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,28 @@
package de.rwu.easydrop.exception;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.Test;
class PersistenceExceptionTest {
@Test
void testPersistenceExceptionWithMessage() {
String errorMessage = "Error occurred during data persistence.";
PersistenceException exception = new PersistenceException(errorMessage);
assertEquals(errorMessage, exception.getMessage());
assertNull(exception.getCause());
}
@Test
void testPersistenceExceptionWithMessageAndCause() {
String errorMessage = "Error occurred during data persistence.";
Throwable cause = new IllegalArgumentException("Invalid argument.");
PersistenceException exception = new PersistenceException(errorMessage, cause);
assertEquals(errorMessage, exception.getMessage());
assertEquals(cause, exception.getCause());
}
}

View File

@@ -0,0 +1,93 @@
package de.rwu.easydrop.model;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ProductCatalogueTest {
private ProductCatalogue productCatalogue;
@BeforeEach
public void setup() {
productCatalogue = new ProductCatalogue("GPU", "Graphics Processing Units");
}
@Test
void testAddProduct() {
Product product = new Product();
product.setProductId("12345");
product.setMerchant("AmazonSeller");
product.setDataOrigin("Amazon");
productCatalogue.addProduct(product);
List<Product> products = productCatalogue.getProducts();
Assertions.assertEquals(1, products.size());
Assertions.assertEquals(product, products.get(0));
}
@Test
void testRemoveProduct() {
Product product1 = new Product();
product1.setProductId("12345");
product1.setMerchant("AmazonSeller");
product1.setDataOrigin("Amazon");
productCatalogue.addProduct(product1);
Product product2 = new Product();
product2.setProductId("54321");
product2.setMerchant("eBaySeller");
product2.setDataOrigin("eBay");
productCatalogue.addProduct(product2);
productCatalogue.removeProduct(product1);
List<Product> products = productCatalogue.getProducts();
Assertions.assertEquals(1, products.size());
Assertions.assertEquals(product2, products.get(0));
}
@Test
void testClearProducts() {
Product product1 = new Product();
product1.setProductId("12345");
product1.setMerchant("AmazonSeller");
product1.setDataOrigin("Amazon");
productCatalogue.addProduct(product1);
Product product2 = new Product();
product2.setProductId("54321");
product2.setMerchant("eBay");
product2.setDataOrigin("eBay");
productCatalogue.addProduct(product2);
productCatalogue.clearProducts();
List<Product> products = productCatalogue.getProducts();
Assertions.assertTrue(products.isEmpty());
}
@Test
void testToString() {
Product product1 = new Product();
product1.setProductId("12345");
product1.setMerchant("AmazonSeller");
product1.setDataOrigin("Amazon");
productCatalogue.addProduct(product1);
Product product2 = new Product();
product2.setProductId("54321");
product2.setMerchant("eBaySeller");
product2.setDataOrigin("eBay");
productCatalogue.addProduct(product2);
String expectedString = "Product Catalogue: GPU\n" +
"Description: Graphics Processing Units\n" +
"Products:\n" +
"Product: [12345 from AmazonSeller (Amazon) at 0,00 € (available: no)]\n" +
"Product: [54321 from eBaySeller (eBay) at 0,00 € (available: no)]\n";
Assertions.assertEquals(expectedString, productCatalogue.toString());
}
}

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,80 @@
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;
}
@Test
void mapProductToDTO() {
// Arrange
Product product = new Product();
product.setProductId("12345");
product.setDataOrigin("Amazon");
product.setAvailable(true);
product.setCurrentPrice(9.99);
product.setDeliveryPrice(2.50);
product.setMerchant("Seller1");
// Act
ProductDTO dto = ProductMapper.mapProductToDTO(product);
// Assert
assertEquals("12345", dto.getProductId());
assertEquals("Amazon", dto.getDataOrigin());
assertTrue(dto.isAvailable());
assertEquals(9.99, dto.getCurrentPrice());
assertEquals(2.50, dto.getDeliveryPrice());
assertEquals("Seller1", dto.getMerchant());
}
}

View File

@@ -0,0 +1,103 @@
package de.rwu.easydrop.service.retriever;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import javax.naming.ConfigurationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import de.rwu.easydrop.exception.InvalidProductException;
import de.rwu.easydrop.model.Product;
import de.rwu.easydrop.model.ProductCatalogue;
import de.rwu.easydrop.util.ProductsConfig;
class CatalogueRetrieverTest {
private ProductsConfig productsConfig;
private ProductRetriever productRetriever;
private CatalogueRetriever catalogueRetriever;
@BeforeEach
public void setup() {
productsConfig = mock(ProductsConfig.class);
productRetriever = mock(ProductRetriever.class);
catalogueRetriever = new CatalogueRetriever(productsConfig, productRetriever);
}
@Test
void loadCatalogues_ValidConfig_CataloguesLoaded() throws ConfigurationException {
// Arrange
List<ProductCatalogue> productCatalogues = new ArrayList<>();
// Create a sample product catalogue with two products
ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue");
Product product1 = new Product();
product1.setDataOrigin("Amazon");
product1.setProductId("ASIN1");
productCatalogue.addProduct(product1);
Product product2 = new Product();
product2.setDataOrigin("eBay");
product2.setProductId("ProductID2");
productCatalogue.addProduct(product2);
productCatalogues.add(productCatalogue);
// Mock the methods
when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues);
when(productRetriever.getProductFromAmazon("ASIN1")).thenReturn(product1);
when(productRetriever.getProductFromEbay("ProductID2")).thenReturn(product2);
// Act
catalogueRetriever.loadCatalogues();
// Assert
List<ProductCatalogue> loadedCatalogues = catalogueRetriever.getProductCatalogues();
assertEquals(1, loadedCatalogues.size());
ProductCatalogue loadedCatalogue = loadedCatalogues.get(0);
assertEquals("Catalogue 1", loadedCatalogue.getProductName());
assertEquals("Sample catalogue", loadedCatalogue.getDescription());
List<Product> loadedProducts = loadedCatalogue.getProducts();
assertEquals(2, loadedProducts.size());
assertEquals(product1, loadedProducts.get(0));
assertEquals(product2, loadedProducts.get(1));
// Verify the method invocations
verify(productsConfig).loadConfig();
verify(productRetriever).getProductFromAmazon("ASIN1");
verify(productRetriever).getProductFromEbay("ProductID2");
}
@Test
void loadCatalogues_ValidConfig_CataloguesLoaded_InvalidProduct() throws ConfigurationException {
// Arrange
List<ProductCatalogue> productCatalogues = new ArrayList<>();
// Create a sample product catalogue
ProductCatalogue productCatalogue = new ProductCatalogue("Catalogue 1", "Sample catalogue");
Product product = new Product();
product.setDataOrigin("");
product.setProductId("ProductID1");
productCatalogue.addProduct(product);
productCatalogues.add(productCatalogue);
// Mock the methods
when(productsConfig.getProductCatalogues()).thenReturn(productCatalogues);
when(productRetriever.getProductFromAmazon("ProductID1")).thenReturn(product);
// Act and Assert
InvalidProductException exception = assertThrows(InvalidProductException.class, () -> {
catalogueRetriever.loadCatalogues();
});
assertEquals("Product data origin is invalid", exception.getMessage());
}
}

View File

@@ -0,0 +1,111 @@
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.data.connector.AbstractProductPersistence;
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;
@Mock
private AbstractProductPersistence persistence;
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);
}
@Test
void getProductFromPersistence_ValidProductId_ReturnsProduct() {
// Arrange
String productId = "123";
when(dataSourceFactory.createProductPersistenceDataSource()).thenReturn(persistence);
when(persistence.getProductDTOById(productId)).thenReturn(productDTO);
when(productDTO.getProductId()).thenReturn(productId);
when(productDTO.getCurrentPrice()).thenReturn(9.99);
when(productDTO.getDataOrigin()).thenReturn("Amazon");
// Act
Product result = productRetriever.getProductFromPersistence(productId);
// Assert
assertEquals(productId, result.getProductId());
assertEquals(9.99, result.getCurrentPrice());
// Verify the interactions
verify(persistence, times(1)).getProductDTOById(productId);
}
}

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

@@ -0,0 +1,69 @@
package de.rwu.easydrop.service.writer;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.data.connector.AbstractProductPersistence;
import de.rwu.easydrop.model.Product;
import de.rwu.easydrop.model.ProductCatalogue;
class CatalogueWriterTest {
@Mock
private AbstractProductPersistence persistenceMock;
private CatalogueWriter catalogueWriter;
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
catalogueWriter = new CatalogueWriter(persistenceMock);
}
@Test
void writeCatalogues_ValidCatalogues_ProductsWrittenToPersistence() {
// Arrange
List<ProductCatalogue> catalogues = createSampleCatalogues();
// Act
catalogueWriter.writeCatalogues(catalogues);
// Assert
verify(persistenceMock, times(4)).saveProduct(any(ProductDTO.class));
}
private List<ProductCatalogue> createSampleCatalogues() {
List<ProductCatalogue> catalogues = new ArrayList<>();
ProductCatalogue catalogue1 = new ProductCatalogue("Catalogue 1", "Sample catalogue 1");
catalogue1.addProduct(createSampleProduct("Amazon", "ID 1"));
catalogue1.addProduct(createSampleProduct("eBay", "ID 2"));
ProductCatalogue catalogue2 = new ProductCatalogue("Catalogue 2", "Sample catalogue 2");
catalogue2.addProduct(createSampleProduct("Amazon", "ID 3"));
catalogue2.addProduct(createSampleProduct("eBay", "ID 4"));
catalogues.add(catalogue1);
catalogues.add(catalogue2);
return catalogues;
}
private Product createSampleProduct(String dataOrigin, String productId) {
Product product = new Product();
product.setDataOrigin(dataOrigin);
product.setProductId(productId);
product.setCurrentPrice(9999.99);
return product;
}
}

View File

@@ -0,0 +1,55 @@
package de.rwu.easydrop.service.writer;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import de.rwu.easydrop.api.dto.ProductDTO;
import de.rwu.easydrop.data.connector.AbstractProductPersistence;
import de.rwu.easydrop.model.Product;
class ProductWriterTest {
@Mock
private AbstractProductPersistence persistence;
private ProductWriter productWriter;
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
productWriter = new ProductWriter();
productWriter.setPersistence(persistence);
}
@Test
void writeProductToPersistence_ValidProduct_CallsSaveProduct() {
// Arrange
Product product = new Product();
product.setProductId("12345");
product.setDataOrigin("Amazon");
product.setCurrentPrice(9.99);
// Act
productWriter.writeProductToPersistence(product);
// Assert
Mockito.verify(persistence).saveProduct(any(ProductDTO.class));
}
@Test
void writeProductToPersistence_InvalidProduct_ThrowsException() {
// Arrange
Product product = new Product();
product.setProductId("");
product.setDataOrigin("Amazon");
// Act and Assert
assertThrows(Exception.class, () -> productWriter.writeProductToPersistence(product));
}
}

View File

@@ -1,6 +1,7 @@
package de.rwu.easydrop.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
@@ -11,9 +12,9 @@ import javax.naming.ConfigurationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ConfigImplTest {
class ConfigIntegrationTest {
private Config config;
private final static String TESTDATA_PATH = "testResources/testdata.properties";
private final static String TESTDATA_PATH = "src/test/resources/test.config.properties";
private final static String TESTDATA_KEY = "API_KEY";
private final static String TESTDATA_VAL = "keyIsHere";
@@ -34,7 +35,9 @@ class ConfigImplTest {
}
@Test
void testGetProperty_ConfigNotLoaded() {
void testGetProperty_ConfigNotLoaded() throws Exception {
config.reset();
NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> {
config.getProperty(TESTDATA_KEY);
});
@@ -74,7 +77,7 @@ class ConfigImplTest {
@Test
void testLoadConfigSuccessfully() {
try {
config.setConfigLocation("testResources/testdata.properties");
config.setConfigLocation("src/test/resources/test.config.properties");
config.loadConfig();
assertEquals(TESTDATA_VAL, config.getProperty(TESTDATA_KEY));
} catch (ConfigurationException e) {
@@ -92,4 +95,19 @@ class ConfigImplTest {
assertEquals("Couldn't load required config file", exception.getMessage());
}
@Test
void testReset() throws ConfigurationException {
config.setConfigLocation("src/test/resources/test.config.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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.spy;
import javax.naming.ConfigurationException;
@@ -20,17 +21,18 @@ class ConfigTest {
}
@Test
void testGetInstanceNull() {
config = null;
void testGetInstance() {
Config newConfig = Config.getInstance();
assertNotNull(newConfig);
}
@Test
void testGetInstanceNotNull() {
Config newConfig = Config.getInstance();
assertNotNull(newConfig);
void testGetInstanceEquality() {
// Create "two" instances to check validity of Singleton pattern
Config instance1 = Config.getInstance();
Config instance2 = Config.getInstance();
assertSame(instance1, instance2, "Instances should be equal");
}
@Test

View File

@@ -0,0 +1,89 @@
package de.rwu.easydrop.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.List;
import javax.naming.ConfigurationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import de.rwu.easydrop.model.ProductCatalogue;
class ProductsConfigIntegrationTest {
private ProductsConfig config;
private final static String TESTDATA_PATH = "src/test/resources/test.products-config.json";
private final static String TESTDATA_EMPTY_PATH = "src/test/resources/test.empty.products-config.json";
private final static String TESTDATA_MALFORMED_PATH = "src/test/resources/test.malformed.products-config.json";
@BeforeEach
void setUp() {
config = ProductsConfig.getInstance();
}
@Test
void testLoadConfigSuccessfully() {
try {
config.setConfigLocation(TESTDATA_PATH);
config.loadConfig();
String value = config.getProductCatalogues().get(0).getProductName();
assertEquals("Demo Product", value);
} catch (ConfigurationException e) {
fail("ConfigurationException should not be thrown");
}
}
@Test
void testLoadConfigMissingFile() {
config.setConfigLocation("path/that/doesnt/exist/products-config.json");
ConfigurationException exception = assertThrows(ConfigurationException.class, () -> {
config.loadConfig();
});
assertEquals("Couldn't load required products config file", exception.getMessage());
}
@Test
void testLoadConfigEmptyFile() {
config.setConfigLocation(TESTDATA_EMPTY_PATH);
ConfigurationException exception = assertThrows(ConfigurationException.class, () -> {
config.loadConfig();
});
assertEquals("Products config is empty or malformed", exception.getMessage());
}
@Test
void testLoadConfigMalformedFile() {
config.setConfigLocation(TESTDATA_MALFORMED_PATH);
ConfigurationException exception = assertThrows(ConfigurationException.class, () -> {
config.loadConfig();
});
assertEquals("Products config is empty or malformed", exception.getMessage());
}
@Test
void testReset() {
try {
config.setConfigLocation(TESTDATA_PATH);
config.loadConfig();
} catch (ConfigurationException e) {
fail("ConfigurationException should not be thrown");
}
assertNotNull(config.getProductCatalogues());
config.reset();
List<ProductCatalogue> pCats = config.getProductCatalogues();
assertEquals(0, pCats.size(), "Catalogues list should be empty");
}
}

View File

@@ -0,0 +1,44 @@
package de.rwu.easydrop.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.spy;
import javax.naming.ConfigurationException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockitoAnnotations;
class ProductsConfigTest {
private ProductsConfig productsConfig;
@BeforeEach
void setUp() throws ConfigurationException {
MockitoAnnotations.openMocks(this);
productsConfig = spy(ProductsConfig.getInstance());
}
@Test
void testGetInstance() {
ProductsConfig newConfig = ProductsConfig.getInstance();
assertNotNull(newConfig);
}
@Test
void testGetInstanceEquality() {
// Create "two" instances to check validity of Singleton pattern
ProductsConfig instance1 = ProductsConfig.getInstance();
ProductsConfig instance2 = ProductsConfig.getInstance();
assertSame(instance1, instance2, "Instances should be equal");
}
@Test
void testSetConfigLocation() {
String newPath = "new/location/config.properties";
productsConfig.setConfigLocation(newPath);
assertEquals(newPath, productsConfig.getConfigLocation());
}
}

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

@@ -0,0 +1 @@

View File

@@ -0,0 +1,15 @@
{
"products": [
{
"name": "Demo Product",
description: "Integration Testing Product",
"identifiers": [
{
"Amazon": "DEMO-AMAZON-001"
},
{
"eBay": "DEMO-EBAY-001"
}
]
]
}

View File

@@ -0,0 +1,16 @@
{
"products": [
{
"name": "Demo Product",
"description": "Integration Testing Product",
"identifiers": [
{
"Amazon": "DEMO-AMAZON-001"
},
{
"eBay": "DEMO-EBAY-001"
}
]
}
]
}

View File

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