Compare commits

...

16 Commits

Author SHA1 Message Date
dbc793cc08 moved scrapy files to unused folder 2021-05-16 21:12:48 +02:00
f1d6487701 . 2021-05-16 15:48:08 +02:00
233a5ba140 Merge remote-tracking branch 'origin/develop' into BETTERZON-58 2021-05-16 15:45:46 +02:00
c7b8b487d2 Merge branch 'develop' into BETTERZON-58 2021-05-16 15:44:21 +02:00
0a11b2b453 moved logic to amazon.py 2021-05-16 15:41:39 +02:00
Patrick
061d1a46e0
BETTERZON-94: Adding API endpoint to deactivate price listings as a vendor manager (#48) 2021-05-16 13:11:22 +02:00
Patrick
b185e4e5e3
BETTERZON-93: Adding API endpoint to get managed shops (#47) 2021-05-16 12:28:11 +02:00
Patrick
cb55cae692
BETTERZON-100: Switching to cookies for session management (#46)
* BETTERZON-100: Switching session handling to cookies

* BETTERZON-100: Some code reformatting

* BETTERZON-100: Some more code reformatting
2021-05-13 18:47:50 +02:00
Patrick
5cc91654c3
BETTERZON-99: Adding some basic cucumber tests (#45) 2021-05-13 16:42:55 +02:00
Patrick
0be394fc1d
BETTERZON-92: Adding API endpoint to edit (update) price alarms (#44) 2021-05-13 00:48:56 +02:00
Patrick
cd0c11dbc7
BETTERZON-91: Adding API endpoint to GET all price alarms for the currently logged in user (#43) 2021-05-13 00:29:01 +02:00
Patrick
f333bbfc05
BETTERZON-90: Adding API endpoint for creating price alarms (#42) 2021-05-12 23:57:24 +02:00
Patrick
3874957d5a
BETTERZON-89: Refactoring / Reformatting and adding unit tests (#41) 2021-05-10 20:11:45 +02:00
Patrick
08de32770d
Adding Codacy code quality badge to README 2021-05-07 09:20:53 +02:00
Patrick
2f74519da3
BETTERZON-76: Adding method descriptions for backend service methods (#40) 2021-05-05 23:20:57 +02:00
d55646a34e Merge branch 'develop' of https://github.com/Mueller-Patrick/Betterzon into develop 2021-05-05 22:15:06 +02:00
51 changed files with 5821 additions and 461 deletions

4984
Backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,9 @@
"author": "",
"license": "ISC",
"dependencies": {
"@types/cookie-parser": "^1.4.2",
"bcrypt": "^5.0.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",

View File

@ -14,6 +14,9 @@ import {vendorsRouter} from './models/vendors/vendors.router';
import {errorHandler} from './middleware/error.middleware';
import {notFoundHandler} from './middleware/notFound.middleware';
import {usersRouter} from './models/users/users.router';
import {pricealarmsRouter} from './models/pricealarms/pricealarms.router';
const cookieParser = require('cookie-parser');
dotenv.config();
@ -38,12 +41,14 @@ const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(cookieParser());
app.use('/products', productsRouter);
app.use('/categories', categoriesRouter);
app.use('/manufacturers', manufacturersRouter);
app.use('/prices', pricesRouter);
app.use('/users', usersRouter);
app.use('/vendors', vendorsRouter);
app.use('/pricealarms', pricealarmsRouter);
app.use(errorHandler);
app.use(notFoundHandler);

View File

@ -1,5 +1,5 @@
import HttpException from "../common/http-exception";
import { Request, Response, NextFunction } from "express";
import HttpException from '../common/http-exception';
import {Request, Response, NextFunction} from 'express';
export const errorHandler = (
error: HttpException,
@ -9,7 +9,7 @@ export const errorHandler = (
) => {
const status = error.statusCode || 500;
const message =
error.message || "It's not you. It's us. We are having some problems.";
error.message || 'It\'s not you. It\'s us. We are having some problems.';
response.status(status).send(message);
};

View File

@ -1,4 +1,4 @@
import { Request, Response, NextFunction } from "express";
import {Request, Response, NextFunction} from 'express';
export const notFoundHandler = (
request: Request,
@ -6,7 +6,7 @@ export const notFoundHandler = (
next: NextFunction
) => {
const message = "Resource not found";
const message = 'Resource not found';
response.status(404).send(message);
};

View File

@ -27,7 +27,7 @@ categoriesRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(categories);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -46,7 +46,7 @@ categoriesRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(category);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -65,6 +65,6 @@ categoriesRouter.get('/search/:term', async (req: Request, res: Response) => {
res.status(200).send(categories);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});

View File

@ -23,6 +23,9 @@ import {Categories} from './categories.interface';
* Service Methods
*/
/**
* Fetches and returns all known categories
*/
export const findAll = async (): Promise<Categories> => {
let conn;
let categRows = [];
@ -54,6 +57,10 @@ export const findAll = async (): Promise<Categories> => {
return categRows;
};
/**
* Fetches and returns the category with the specified id
* @param id The id of the category to fetch
*/
export const find = async (id: number): Promise<Category> => {
let conn;
let categ: any;
@ -77,6 +84,10 @@ export const find = async (id: number): Promise<Category> => {
return categ;
};
/**
* Fetches and returns all categories that match the search term
* @param term the term to match
*/
export const findBySearchTerm = async (term: string): Promise<Categories> => {
let conn;
let categRows = [];
@ -100,36 +111,3 @@ export const findBySearchTerm = async (term: string): Promise<Categories> => {
return categRows;
};
// export const create = async (newItem: Product): Promise<void> => {
// let conn;
// try {
// conn = await pool.getConnection();
// await conn.query("");
//
// } catch (err) {
// throw err;
// } finally {
// if (conn) conn.end();
// }
// };
//
// export const update = async (updatedItem: Product): Promise<void> => {
// if (models.products[updatedItem.product_id]) {
// models.products[updatedItem.product_id] = updatedItem;
// return;
// }
//
// throw new Error("No record found to update");
// };
//
// export const remove = async (id: number): Promise<void> => {
// const record: Product = models.products[id];
//
// if (record) {
// delete models.products[id];
// return;
// }
//
// throw new Error("No record found to delete");
// };

View File

@ -27,7 +27,7 @@ manufacturersRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(manufacturers);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -46,11 +46,11 @@ manufacturersRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(manufacturer);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// GET items/:name
// GET items/:term
manufacturersRouter.get('/search/:term', async (req: Request, res: Response) => {
const term: string = req.params.term;
@ -65,6 +65,6 @@ manufacturersRouter.get('/search/:term', async (req: Request, res: Response) =>
res.status(200).send(manufacturer);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});

View File

@ -23,6 +23,9 @@ import {Manufacturers} from './manufacturers.interface';
* Service Methods
*/
/**
* Fetches and returns all known manufacturers
*/
export const findAll = async (): Promise<Manufacturers> => {
let conn;
let manRows = [];
@ -54,6 +57,10 @@ export const findAll = async (): Promise<Manufacturers> => {
return manRows;
};
/**
* Fetches and returns the manufacturer with the specified id
* @param id The id of the manufacturer to fetch
*/
export const find = async (id: number): Promise<Manufacturer> => {
let conn;
let man: any;
@ -77,6 +84,10 @@ export const find = async (id: number): Promise<Manufacturer> => {
return man;
};
/**
* Fetches and returns all manufacturers that match the search term
* @param term the term to match
*/
export const findBySearchTerm = async (term: string): Promise<Manufacturers> => {
let conn;
let manRows = [];
@ -100,36 +111,3 @@ export const findBySearchTerm = async (term: string): Promise<Manufacturers> =>
return manRows;
};
// export const create = async (newItem: Product): Promise<void> => {
// let conn;
// try {
// conn = await pool.getConnection();
// await conn.query("");
//
// } catch (err) {
// throw err;
// } finally {
// if (conn) conn.end();
// }
// };
//
// export const update = async (updatedItem: Product): Promise<void> => {
// if (models.products[updatedItem.product_id]) {
// models.products[updatedItem.product_id] = updatedItem;
// return;
// }
//
// throw new Error("No record found to update");
// };
//
// export const remove = async (id: number): Promise<void> => {
// const record: Product = models.products[id];
//
// if (record) {
// delete models.products[id];
// return;
// }
//
// throw new Error("No record found to delete");
// };

View File

@ -0,0 +1,6 @@
export interface PriceAlarm {
alarm_id: number;
user_id: number;
product_id: number;
defined_price: number;
}

View File

@ -0,0 +1,5 @@
import {PriceAlarm} from './pricealarm.interface';
export interface PriceAlarms {
[key: number]: PriceAlarm;
}

View File

@ -0,0 +1,102 @@
/**
* Required External Modules and Interfaces
*/
import express, {Request, Response} from 'express';
import * as PriceAlarmsService from './pricealarms.service';
import {PriceAlarm} from './pricealarm.interface';
import {PriceAlarms} from './pricealarms.interface';
import * as UserService from '../users/users.service';
/**
* Router Definition
*/
export const pricealarmsRouter = express.Router();
/**
* Controller Definitions
*/
//GET pricealarms/
pricealarmsRouter.get('/', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip);
const priceAlarms = await PriceAlarmsService.getPriceAlarms(user.user_id);
res.status(200).send(priceAlarms);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// POST pricealarms/create
pricealarmsRouter.post('/create', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip);
// Get info for price alarm creation
const product_id = req.body.product_id;
const defined_price = req.body.defined_price;
if (!product_id || !defined_price) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Create price alarm
const success = await PriceAlarmsService.createPriceAlarm(user.user_id, product_id, defined_price);
if (success) {
res.status(201).send(JSON.stringify({success: true}));
return;
} else {
res.status(500).send(JSON.stringify({success: false}));
return;
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// PUT pricealarms/update
pricealarmsRouter.put('/update', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip);
// Get info for price alarm creation
const alarm_id = req.body.alarm_id;
const defined_price = req.body.defined_price;
if (!alarm_id || !defined_price) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Create price alarm
const success = await PriceAlarmsService.updatePriceAlarm(alarm_id, user.user_id, defined_price);
if (success) {
res.status(201).send(JSON.stringify({success: true}));
return;
} else {
res.status(500).send(JSON.stringify({success: false}));
return;
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});

View File

@ -0,0 +1,106 @@
import * as dotenv from 'dotenv';
dotenv.config();
const mariadb = require('mariadb');
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
connectionLimit: 5
});
/**
* Data Model Interfaces
*/
import {PriceAlarm} from './pricealarm.interface';
import {PriceAlarms} from './pricealarms.interface';
/**
* Service Methods
*/
/**
* Creates a price alarm for the given user for the product with the defined price
* @param user_id The id of the user to create the price alarm for
* @param product_id The id of the product to create the price alarm for
* @param defined_price The defined price for the price alarm
*/
export const createPriceAlarm = async (user_id: number, product_id: number, defined_price: number): Promise<Boolean> => {
let conn;
try {
conn = await pool.getConnection();
const res = await conn.query('INSERT INTO price_alarms (user_id, product_id, defined_price) VALUES (?, ?, ?)', [user_id, product_id, defined_price]);
if (res.affectedRows === 1) {
return true;
} else {
return false;
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return false;
};
/**
* Fetches and returns all price alarms for the given user
* @param user_id
*/
export const getPriceAlarms = async (user_id: number): Promise<PriceAlarms> => {
let conn;
let priceAlarms = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT alarm_id, user_id, product_id, defined_price FROM price_alarms WHERE user_id = ?', user_id);
for (let row in rows) {
if (row !== 'meta') {
priceAlarms.push(rows[row]);
}
}
return priceAlarms;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
};
/**
* Updates the given price alarm with the given fields
* @param alarm_id The id of the price alarm to update
* @param user_id The id of the user that wants to update the price alarm
* @param defined_price The defined price for the price alarm
*/
export const updatePriceAlarm = async (alarm_id: number, user_id: number, defined_price: number): Promise<Boolean> => {
let conn;
try {
conn = await pool.getConnection();
const res = await conn.query('UPDATE price_alarms SET defined_price = ? WHERE alarm_id = ? AND user_id = ?', [defined_price, alarm_id, user_id]);
if (res.affectedRows === 1) {
return true;
} else {
return false;
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return false;
};

View File

@ -40,7 +40,7 @@ pricesRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(prices);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -59,7 +59,7 @@ pricesRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(price);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -78,7 +78,7 @@ pricesRouter.get('/bestDeals/:amount', async (req: Request, res: Response) => {
res.status(200).send(prices);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -97,6 +97,6 @@ pricesRouter.get('/byProduct/list/:ids', async (req: Request, res: Response) =>
res.status(200).send(prices);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});

View File

@ -23,12 +23,15 @@ import {Prices} from './prices.interface';
* Service Methods
*/
/**
* Fetches and returns all known prices
*/
export const findAll = async (): Promise<Prices> => {
let conn;
let priceRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices');
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE active_listing = true');
for (let row in rows) {
if (row !== 'meta') {
let price: Price = {
@ -60,12 +63,16 @@ export const findAll = async (): Promise<Prices> => {
return priceRows;
};
/**
* Fetches and returns the price with the specified id
* @param id The id of the price to fetch
*/
export const find = async (id: number): Promise<Price> => {
let conn;
let price: any;
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE price_id = ?', id);
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE price_id = ? AND active_listing = true', id);
for (let row in rows) {
if (row !== 'meta') {
price = rows[row];
@ -83,12 +90,16 @@ export const find = async (id: number): Promise<Price> => {
return price;
};
/**
* Fetches and returns all prices that belong to the specified product
* @param product the product to fetch the prices for
*/
export const findByProduct = async (product: number): Promise<Prices> => {
let conn;
let priceRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ?', product);
const rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND active_listing = true', product);
for (let row in rows) {
if (row !== 'meta') {
priceRows.push(rows[row]);
@ -106,6 +117,14 @@ export const findByProduct = async (product: number): Promise<Prices> => {
return priceRows;
};
/**
* Fetches and returns prices that belong to the specified product.
* If type is newest, only the newest prices for each vendor will be returned.
* If type is lowest, the lowest daily price for the product is returned.
* Otherwise, all prices for this product are returned.
* @param product The product to fetch the prices for
* @param type The type of prices, e.g. newest / lowest
*/
export const findByType = async (product: string, type: string): Promise<Prices> => {
let conn;
let priceRows = [];
@ -123,16 +142,16 @@ export const findByType = async (product: string, type: string): Promise<Prices>
'PARTITION BY p.vendor_id ' +
'ORDER BY p.timestamp DESC) AS rk ' +
'FROM prices p ' +
'WHERE product_id = ? AND vendor_id != 1) ' +
'WHERE product_id = ? AND vendor_id != 1 AND active_listing = true) ' +
'SELECT s.* ' +
'FROM summary s ' +
'WHERE s.rk = 1 '), product);
} else if (type === 'lowest') {
// Used to get the lowest prices for this product over a period of time
rows = await conn.query('SELECT price_id, product_id, vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id != 1 GROUP BY DAY(timestamp) ORDER BY timestamp', product);
rows = await conn.query('SELECT price_id, product_id, vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id != 1 AND active_listing = true GROUP BY DAY(timestamp) ORDER BY timestamp', product);
} else {
// If no type is given, return all prices for this product
rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id != 1', product);
rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id != 1 AND active_listing = true', product);
}
for (let row in rows) {
@ -152,6 +171,15 @@ export const findByType = async (product: string, type: string): Promise<Prices>
return priceRows;
};
/**
* Fetches and returns prices that belong to the specified product and vendor.
* If type is newest, only the newest known price for the product at the vendor is returned.
* If type is lowest, only the lowest ever known price for the product at the vendor is returned.
* Otherwise, all prices for this product are returned.
* @param product The product to fetch the prices for
* @param vendor The vendor to fetch the prices for
* @param type The type of prices, e.g. newest / lowest
*/
export const findByVendor = async (product: string, vendor: string, type: string): Promise<Prices> => {
let conn;
let priceRows = [];
@ -160,13 +188,13 @@ export const findByVendor = async (product: string, vendor: string, type: string
let rows = [];
if (type === 'newest') {
// Used to get the newest price for this product and vendor
rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ? ORDER BY timestamp DESC LIMIT 1', [product, vendor]);
rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ? AND active_listing = true ORDER BY timestamp DESC LIMIT 1', [product, vendor]);
} else if (type === 'lowest') {
// Used to get the lowest prices for this product and vendor in all time
rows = await conn.query('SELECT price_id, product_id, vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ? LIMIT 1', [product, vendor]);
rows = await conn.query('SELECT price_id, product_id, vendor_id, MIN(price_in_cents) as price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ? AND active_listing = true LIMIT 1', [product, vendor]);
} else {
// If no type is given, return all prices for this product and vendor
rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ?', [product, vendor]);
rows = await conn.query('SELECT price_id, product_id, vendor_id, price_in_cents, timestamp FROM prices WHERE product_id = ? AND vendor_id = ? AND active_listing = true', [product, vendor]);
}
for (let row in rows) {
@ -186,6 +214,11 @@ export const findByVendor = async (product: string, vendor: string, type: string
return priceRows;
};
/**
* Fetches and returns the best current deals, i.e. the non-amazon prices that have the biggest difference to amazon prices.
* Only the latest known prices for every vendor are taken into consideration so we only get up-to-date-deals.
* @param amount The amount of deals to return
*/
export const getBestDeals = async (amount: number): Promise<Prices> => {
let conn;
let priceRows = [];
@ -204,7 +237,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
' ROW_NUMBER() OVER(\n' +
' PARTITION BY p.product_id, p.vendor_id\n' +
' ORDER BY p.timestamp DESC) AS rk\n' +
' FROM prices p)\n' +
' FROM prices p WHERE active_listing = true)\n' +
'SELECT s.*\n' +
'FROM summary s\n' +
'WHERE s.rk = 1');
@ -265,7 +298,6 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
let maxAmt = Math.min(amount, deals.length);
for (let dealIndex = 0; dealIndex < maxAmt; dealIndex++) {
//console.log(deals[dealIndex]);
priceRows.push(deals[dealIndex] as Price);
}
@ -282,7 +314,7 @@ export const getBestDeals = async (amount: number): Promise<Prices> => {
};
/**
* Get the lowest, latest, non-amazon price for each given product
* Fetches and returns the lowest, latest, non-amazon price for each given product
* @param ids the ids of the products
*/
export const findListByProducts = async (productIds: [number]): Promise<Prices> => {
@ -305,7 +337,7 @@ export const findListByProducts = async (productIds: [number]): Promise<Prices>
' ORDER BY p.timestamp DESC) AS rk\n' +
' FROM prices p' +
' WHERE p.product_id IN (?)' +
' AND p.vendor_id != 1)\n' +
' AND p.vendor_id != 1 AND active_listing = true)\n' +
'SELECT s.*\n' +
'FROM summary s\n' +
'WHERE s.rk = 1', [productIds]);
@ -344,36 +376,3 @@ export const findListByProducts = async (productIds: [number]): Promise<Prices>
return priceRows;
};
// export const create = async (newItem: Product): Promise<void> => {
// let conn;
// try {
// conn = await pool.getConnection();
// await conn.query("");
//
// } catch (err) {
// throw err;
// } finally {
// if (conn) conn.end();
// }
// };
//
// export const update = async (updatedItem: Product): Promise<void> => {
// if (models.products[updatedItem.product_id]) {
// models.products[updatedItem.product_id] = updatedItem;
// return;
// }
//
// throw new Error("No record found to update");
// };
//
// export const remove = async (id: number): Promise<void> => {
// const record: Product = models.products[id];
//
// if (record) {
// delete models.products[id];
// return;
// }
//
// throw new Error("No record found to delete");
// };

View File

@ -27,7 +27,7 @@ productsRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(products);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -46,7 +46,7 @@ productsRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(product);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -65,7 +65,7 @@ productsRouter.get('/search/:term', async (req: Request, res: Response) => {
res.status(200).send(products);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -84,6 +84,6 @@ productsRouter.get('/list/:ids', async (req: Request, res: Response) => {
res.status(200).send(products);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});

View File

@ -23,6 +23,9 @@ import {Products} from './products.interface';
* Service Methods
*/
/**
* Fetches and returns all known products
*/
export const findAll = async (): Promise<Products> => {
let conn;
let prodRows = [];
@ -74,6 +77,10 @@ export const findAll = async (): Promise<Products> => {
return prodRows;
};
/**
* Fetches and returns the product with the specified id
* @param id The id of the product to fetch
*/
export const find = async (id: number): Promise<Product> => {
let conn;
let prod: any;
@ -97,6 +104,10 @@ export const find = async (id: number): Promise<Product> => {
return prod;
};
/**
* Fetches and returns all products that match the search term
* @param term the term to match
*/
export const findBySearchTerm = async (term: string): Promise<Products> => {
let conn;
let prodRows = [];
@ -122,6 +133,10 @@ export const findBySearchTerm = async (term: string): Promise<Products> => {
return prodRows;
};
/**
* Fetches and returns the product details for the given list of product ids
* @param ids The list of product ids to fetch the details for
*/
export const findList = async (ids: [number]): Promise<Products> => {
let conn;
let prodRows = [];
@ -144,36 +159,3 @@ export const findList = async (ids: [number]): Promise<Products> => {
return prodRows;
};
// export const create = async (newItem: Product): Promise<void> => {
// let conn;
// try {
// conn = await pool.getConnection();
// await conn.query("");
//
// } catch (err) {
// throw err;
// } finally {
// if (conn) conn.end();
// }
// };
//
// export const update = async (updatedItem: Product): Promise<void> => {
// if (models.products[updatedItem.product_id]) {
// models.products[updatedItem.product_id] = updatedItem;
// return;
// }
//
// throw new Error("No record found to update");
// };
//
// export const remove = async (id: number): Promise<void> => {
// const record: Product = models.products[id];
//
// if (record) {
// delete models.products[id];
// return;
// }
//
// throw new Error("No record found to delete");
// };

View File

@ -47,10 +47,13 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
const session: Session = await UserService.createUser(username, password, email, ip);
// Send the session details back to the user
res.status(201).send(session);
res.cookie('betterauth', JSON.stringify({
id: session.session_id,
key: session.session_key
}), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)}).sendStatus(201);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
@ -70,39 +73,34 @@ usersRouter.post('/login', async (req: Request, res: Response) => {
// Update the user entry and create a session
const session: Session = await UserService.login(username, password, ip);
if(!session.session_id) {
if (!session.session_id) {
// Error logging in, probably wrong username / password
res.status(401).send(JSON.stringify({messages: ["Wrong username and / or password"], codes: [1, 4]}));
res.status(401).send(JSON.stringify({messages: ['Wrong username and / or password'], codes: [1, 4]}));
return;
}
// Send the session details back to the user
res.status(201).send(session);
res.cookie('betterauth', JSON.stringify({
id: session.session_id,
key: session.session_key
}), {expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)}).sendStatus(200);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// POST users/checkSessionValid
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
try {
const sessionId: string = req.body.sessionId;
const sessionKey: string = req.body.sessionKey;
const ip: string = req.connection.remoteAddress ?? '';
if (!sessionId || !sessionKey) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
// Update the user entry and create a session
const user: User = await UserService.checkSession(sessionId, sessionKey, ip);
const user: User = await UserService.checkSessionWithCookie(req.cookies.betterauth, ip);
if(!user.user_id) {
if (!user.user_id) {
// Error logging in, probably wrong username / password
res.status(401).send(JSON.stringify({messages: ["Invalid session"], codes: [5]}));
res.status(401).send(JSON.stringify({messages: ['Invalid session'], codes: [5]}));
return;
}
@ -110,6 +108,6 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
res.status(201).send(user);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});

View File

@ -68,7 +68,7 @@ export const createUser = async (username: string, password: string, email: stri
return {
session_id: sessionId,
session_key: sessionKey,
session_key_hash: '',
session_key_hash: 'HIDDEN',
last_IP: ip
};
@ -135,7 +135,7 @@ export const login = async (username: string, password: string, ip: string): Pro
return {
session_id: sessionId,
session_key: sessionKey,
session_key_hash: '',
session_key_hash: 'HIDDEN',
last_IP: ip
};
@ -179,7 +179,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
// Key is valid, continue
// Check if the session is still valid
if(validUntil <= new Date()) {
if (validUntil <= new Date()) {
// Session expired, return invalid
return {} as User;
}
@ -193,7 +193,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
await conn.commit();
// Get the other required user information and update the user
const userQuery = "SELECT user_id, username, email, registration_date, last_login_date FROM users WHERE user_id = ?";
const userQuery = 'SELECT user_id, username, email, registration_date, last_login_date FROM users WHERE user_id = ?';
const userRows = await conn.query(userQuery, userId);
let username = '';
let email = '';
@ -213,7 +213,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
user_id: userId,
username: username,
email: email,
password_hash: '',
password_hash: 'HIDDEN',
registration_date: registrationDate,
last_login_date: lastLoginDate
};
@ -229,6 +229,20 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
return {} as User;
};
/**
* Calls the checkSession method after extracting the required information from the authentication cookie
* @param cookie The betterauth cookie
* @param ip The users IP address
*/
export const checkSessionWithCookie = async (cookie: any, ip: string): Promise<User> => {
const parsedCookie = JSON.parse(cookie);
const session_id = parsedCookie.id;
const session_key = parsedCookie.key;
return checkSession(session_id, session_key, '');
};
/**
* Used in the checkUsernameAndEmail method as return value
*/

View File

@ -6,6 +6,7 @@ import express, {Request, Response} from 'express';
import * as VendorService from './vendors.service';
import {Vendor} from './vendor.interface';
import {Vendors} from './vendors.interface';
import * as UserService from '../users/users.service';
/**
@ -19,7 +20,7 @@ export const vendorsRouter = express.Router();
* Controller Definitions
*/
// GET items/
// GET vendors/
vendorsRouter.get('/', async (req: Request, res: Response) => {
try {
const vendors: Vendors = await VendorService.findAll();
@ -27,11 +28,27 @@ vendorsRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(vendors);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// GET items/:id
// GET vendors/managed
vendorsRouter.get('/managed', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip);
const vendors = await VendorService.getManagedShops(user.user_id);
res.status(200).send(vendors);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// GET vendors/:id
vendorsRouter.get('/:id', async (req: Request, res: Response) => {
const id: number = parseInt(req.params.id, 10);
@ -46,11 +63,11 @@ vendorsRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(vendor);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// GET items/:name
// GET vendors/search/:term
vendorsRouter.get('/search/:term', async (req: Request, res: Response) => {
const term: string = req.params.term;
@ -65,6 +82,30 @@ vendorsRouter.get('/search/:term', async (req: Request, res: Response) => {
res.status(200).send(vendors);
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({"message": "Internal Server Error. Try again later."}));
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});
// PUT /manage/deactivatelisting
vendorsRouter.put('/manage/deactivatelisting', async (req: Request, res: Response) => {
try {
// Authenticate user
const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip);
// Get required parameters
const vendor_id = req.body.vendor_id;
const product_id = req.body.product_id;
const success = await VendorService.deactivateListing(user.user_id, vendor_id, product_id);
if(success) {
res.sendStatus(200);
} else {
res.sendStatus(500);
}
} catch (e) {
console.log('Error handling a request: ' + e.message);
res.status(500).send(JSON.stringify({'message': 'Internal Server Error. Try again later.'}));
}
});

View File

@ -17,12 +17,16 @@ const pool = mariadb.createPool({
import {Vendor} from './vendor.interface';
import {Vendors} from './vendors.interface';
import {User} from '../users/user.interface';
/**
* Service Methods
*/
/**
* Fetches and returns all known vendors
*/
export const findAll = async (): Promise<Vendors> => {
let conn;
let vendorRows = [];
@ -66,6 +70,10 @@ export const findAll = async (): Promise<Vendors> => {
return vendorRows;
};
/**
* Fetches and returns the vendor with the specified id
* @param id The id of the vendor to fetch
*/
export const find = async (id: number): Promise<Vendor> => {
let conn;
let vendor: any;
@ -89,6 +97,10 @@ export const find = async (id: number): Promise<Vendor> => {
return vendor;
};
/**
* Fetches and returns all vendors that match the search term
* @param term the term to match
*/
export const findBySearchTerm = async (term: string): Promise<Vendors> => {
let conn;
let vendorRows = [];
@ -113,35 +125,63 @@ export const findBySearchTerm = async (term: string): Promise<Vendors> => {
return vendorRows;
};
// export const create = async (newItem: Product): Promise<void> => {
// let conn;
// try {
// conn = await pool.getConnection();
// await conn.query("");
//
// } catch (err) {
// throw err;
// } finally {
// if (conn) conn.end();
// }
// };
//
// export const update = async (updatedItem: Product): Promise<void> => {
// if (models.products[updatedItem.product_id]) {
// models.products[updatedItem.product_id] = updatedItem;
// return;
// }
//
// throw new Error("No record found to update");
// };
//
// export const remove = async (id: number): Promise<void> => {
// const record: Product = models.products[id];
//
// if (record) {
// delete models.products[id];
// return;
// }
//
// throw new Error("No record found to delete");
// };
/**
* Get all vendors that have the given user as admin
* @param user The user to return the managed shops for
*/
export const getManagedShops = async (user_id: number): Promise<Vendors> => {
let conn;
let vendorRows = [];
try {
conn = await pool.getConnection();
const rows = await conn.query('SELECT vendor_id, name, streetname, zip_code, city, country_code, phone, website FROM vendors WHERE admin_id LIKE ?', user_id);
for (let row in rows) {
if (row !== 'meta') {
vendorRows.push(rows[row]);
}
}
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return vendorRows;
};
/**
* Deactivates a product listing for a specific vendor
* @param user_id The user id of the issuing user
* @param vendor_id The vendor id of the vendor to deactivate the listing for
* @param product_id The product id of the product to deactivate the listing for
*/
export const deactivateListing = async (user_id: number, vendor_id: number, product_id: number): Promise<Boolean> => {
let conn;
try {
conn = await pool.getConnection();
// Check if the user is authorized to manage the requested vendor
const user_vendor_rows = await conn.query('SELECT vendor_id FROM vendors WHERE vendor_id = ? AND admin_id = ?', [vendor_id, user_id]);
if (user_vendor_rows.length !== 1) {
return false;
}
const status = await conn.query('UPDATE prices SET active_listing = false WHERE vendor_id = ? and product_id = ?', [vendor_id, product_id]);
if(status.affectedRows > 0){
return true;
}
return false;
} catch (err) {
throw err;
} finally {
if (conn) {
conn.end();
}
}
return false;
};

View File

@ -1,32 +1,32 @@
const webpack = require("webpack");
const path = require("path");
const nodeExternals = require("webpack-node-externals");
const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
entry: ["webpack/hot/poll?100", "./src/index.ts"],
entry: ['webpack/hot/poll?100', './src/index.ts'],
watch: false,
target: "node",
target: 'node',
externals: [
nodeExternals({
whitelist: ["webpack/hot/poll?100"]
whitelist: ['webpack/hot/poll?100']
})
],
module: {
rules: [
{
test: /.tsx?$/,
use: "ts-loader",
use: 'ts-loader',
exclude: /node_modules/
}
]
},
mode: "development",
mode: 'development',
resolve: {
extensions: [".tsx", ".ts", ".js"]
extensions: ['.tsx', '.ts', '.js']
},
plugins: [new webpack.HotModuleReplacementPlugin()],
output: {
path: path.join(__dirname, "dist"),
filename: "index.js"
path: path.join(__dirname, 'dist'),
filename: 'index.js'
}
};

View File

@ -2,13 +2,13 @@
<module type="WEB_MODULE" version="4">
<component name="FacetManager">
<facet type="Python" name="Python">
<configuration sdkName="Python 3.9" />
<configuration sdkName="Python 3.9 (venv)" />
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Python 3.9 interpreter library" level="application" />
<orderEntry type="library" name="Python 3.9 (venv) interpreter library" level="application" />
</component>
</module>

View File

@ -1,4 +1,5 @@
import sql
import amazonspider
def crawl(product_ids: [int]) -> dict:
@ -50,13 +51,14 @@ def crawl(product_ids: [int]) -> dict:
'products_with_problems': products_with_problems
}
def __crawl_amazon__(product_info: dict) -> tuple:
"""
Crawls the price for the given product from amazon
:param product_info: A dict with product info containing product_id, vendor_id, url
:return: A tuple with the crawled data, containing (product_id, vendor_id, price_in_cents)
"""
amazonspider.start_crawling()
return (product_info['product_id'], product_info['vendor_id'], 123)

View File

@ -0,0 +1,33 @@
import scrapy
from scrapy.crawler import CrawlerProcess
import re
class AmazonSpider(scrapy.Spider):
name = 'amazon'
allowed_domains = ['amazon.de']
start_urls = ['https://amazon.de/dp/B083DRCPJG']
# def __init__(self, start_urls):
# self.start_urls = start_urls
def parse(self, response):
price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first()
if not price:
price = response.xpath('//*[@data-asin-price]/@data-asin-price').extract_first() or \
response.xpath('//*[@id="price_inside_buybox"]/text()').extract_first()
euros = re.match('(\d*),\d\d', price).group(1)
cents = re.match('\d*,(\d\d)', price).group(1)
priceincents = euros + cents
yield {'price': priceincents}
def start_crawling():
process = CrawlerProcess(
settings={'COOKIES_ENABLED': 'False', 'CONCURRENT_REQUESTS_PER_IP': 1, 'ROBOTSTXT_OBEY': False,
'USER_AGENT': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
'DOWNLOAD_DELAY': 3}
, install_root_handler=False)
process.crawl()
process.start()

View File

@ -8,4 +8,4 @@ default = crawler.settings
[deploy]
#url = http://localhost:6800/
project = crawler
project = crawler

View File

@ -0,0 +1,25 @@
import scrapy
import re
class AmazonSpider(scrapy.Spider):
name = 'amazon'
allowed_domains = ['amazon.de']
start_urls = ['https://amazon.de/dp/B083DRCPJG']
def parse(self, response):
price = response.xpath('//*[@id="priceblock_ourprice"]/text()').extract_first()
if not price:
price = response.xpath('//*[@data-asin-price]/@data-asin-price').extract_first() or \
response.xpath('//*[@id="price_inside_buybox"]/text()').extract_first()
euros = re.match('(\d*),\d\d', price).group(1)
cents = re.match('\d*,(\d\d)', price).group(1)
priceincents = euros + cents
yield {'price': priceincents}

View File

@ -10,17 +10,24 @@
<sourceFolder url="file://$MODULE_DIR$/src/test/resource" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="jdk" jdkName="openjdk-16" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-java:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-core:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: info.cukes:cucumber-html:0.2.6" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-jvm-deps:1.0.6" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:gherkin:5.0.0" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:tag-expressions:1.1.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: io.cucumber:cucumber-junit:2.3.1" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-java:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-core:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-gherkin:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-gherkin-messages:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:messages:15.0.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:tag-expressions:3.0.1" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-expressions:10.3.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:datatable:3.5.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-plugin:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:docstring:6.10.3" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:html-formatter:13.0.0" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:create-meta:4.0.0" level="project" />
<orderEntry type="library" name="Maven: org.apiguardian:apiguardian-api:1.1.1" level="project" />
<orderEntry type="library" name="Maven: io.cucumber:cucumber-junit:6.10.3" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven.plugins:maven-compiler-plugin:3.8.1" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven:maven-plugin-api:3.0" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven:maven-model:3.0" level="project" />
@ -53,5 +60,25 @@
<orderEntry type="library" name="Maven: org.codehaus.plexus:plexus-compiler-api:2.8.4" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.plexus:plexus-compiler-manager:2.8.4" level="project" />
<orderEntry type="library" scope="RUNTIME" name="Maven: org.codehaus.plexus:plexus-compiler-javac:2.8.4" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-java:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-api:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-chrome-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-edge-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-firefox-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-ie-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-opera-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-remote-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-safari-driver:3.141.59" level="project" />
<orderEntry type="library" name="Maven: org.seleniumhq.selenium:selenium-support:3.141.59" level="project" />
<orderEntry type="library" name="Maven: net.bytebuddy:byte-buddy:1.8.15" level="project" />
<orderEntry type="library" name="Maven: org.apache.commons:commons-exec:1.3" level="project" />
<orderEntry type="library" name="Maven: com.google.guava:guava:25.0-jre" level="project" />
<orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" />
<orderEntry type="library" name="Maven: org.checkerframework:checker-compat-qual:2.0.0" level="project" />
<orderEntry type="library" name="Maven: com.google.errorprone:error_prone_annotations:2.1.3" level="project" />
<orderEntry type="library" name="Maven: com.google.j2objc:j2objc-annotations:1.1" level="project" />
<orderEntry type="library" name="Maven: org.codehaus.mojo:animal-sniffer-annotations:1.14" level="project" />
<orderEntry type="library" name="Maven: com.squareup.okhttp3:okhttp:3.11.0" level="project" />
<orderEntry type="library" name="Maven: com.squareup.okio:okio:1.14.0" level="project" />
</component>
</module>

View File

@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.taskhub</groupId>
<groupId>xyz.betterzon</groupId>
<artifactId>CucumberTests</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
@ -13,21 +13,30 @@
</properties>
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>2.3.1</version>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>6.10.3</version>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>2.3.1</version>
<scope>test</scope>
<version>6.10.3</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
</dependencies>
</project>
</project>

View File

@ -1,6 +1,10 @@
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.openqa.selenium.firefox.FirefoxDriver;
import stepdefs.Preconditions;
@RunWith(Cucumber.class)
@CucumberOptions(
@ -9,4 +13,13 @@ import org.junit.runner.RunWith;
)
public class RunTest {
@BeforeClass
public static void setup() {
Preconditions.driver= new FirefoxDriver();
}
@AfterClass
public static void teardown() {
Preconditions.driver.close();
}
}

View File

@ -0,0 +1,7 @@
package stepdefs;
import org.openqa.selenium.WebDriver;
public class Preconditions {
public static WebDriver driver;
}

View File

@ -1,67 +1,68 @@
package stepdefs;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import io.cucumber.java.PendingException;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
public class PriceAlarm {
@Given("^the user has at least (\\d+) price alarm set$")
public void the_user_has_at_least_price_alarm_set(int arg1) throws Exception {
}
@Given("^the user has at least (\\d+) price alarm set$")
public void the_user_has_at_least_price_alarm_set(int arg1) throws Exception {
}
@When("^the user clicks on the profile icon$")
public void the_user_clicks_on_the_profile_icon() throws Exception {
}
@When("^the user clicks on the profile icon$")
public void the_user_clicks_on_the_profile_icon() throws Exception {
}
@Then("^the profile details popup should open$")
public void the_profile_details_popup_should_open() throws Exception {
}
@Then("^the profile details popup should open$")
public void the_profile_details_popup_should_open() throws Exception {
}
@When("^the user clicks on price alarms$")
public void the_user_clicks_on_price_alarms() throws Exception {
}
@When("^the user clicks on price alarms$")
public void the_user_clicks_on_price_alarms() throws Exception {
}
@Then("^the price alarm list should open$")
public void the_price_alarm_list_should_open() throws Exception {
}
@Then("^the price alarm list should open$")
public void the_price_alarm_list_should_open() throws Exception {
}
@Then("^the price alarm list should contain at least (\\d+) entry$")
public void the_price_alarm_list_should_contain_at_least_entry(int arg1) throws Exception {
}
@Then("^the price alarm list should contain at least (\\d+) entry$")
public void the_price_alarm_list_should_contain_at_least_entry(int arg1) throws Exception {
}
@Then("^the price alarm list should contain a maximum of (\\d+) entries per page$")
public void the_price_alarm_list_should_contain_a_maximum_of_entries_per_page(int arg1) throws Exception {
}
@Then("^the price alarm list should contain a maximum of (\\d+) entries per page$")
public void the_price_alarm_list_should_contain_a_maximum_of_entries_per_page(int arg1) throws Exception {
}
@Given("^the user is on the price alarm list page$")
public void the_user_is_on_the_price_alarm_list_page() throws Exception {
}
@Given("^the user is on the price alarm list page$")
public void the_user_is_on_the_price_alarm_list_page() throws Exception {
}
@When("^the user clicks on the \"([^\"]*)\" button next to a price alarm$")
public void the_user_clicks_on_the_button_next_to_a_price_alarm(String arg1) throws Exception {
}
@When("^the user clicks on the \"([^\"]*)\" button next to a price alarm$")
public void the_user_clicks_on_the_button_next_to_a_price_alarm(String arg1) throws Exception {
}
@Then("^a popup should open asking the user to confirm the removal$")
public void a_popup_should_open_asking_the_user_to_confirm_the_removal() throws Exception {
}
@Then("^a popup should open asking the user to confirm the removal$")
public void a_popup_should_open_asking_the_user_to_confirm_the_removal() throws Exception {
}
@When("^the user confirms the removal of the price alarm$")
public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception {
}
@When("^the user confirms the removal of the price alarm$")
public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception {
}
@Then("^the price alarm should be removed from the database$")
public void the_price_alarm_should_be_removed_from_the_database() throws Exception {
}
@Then("^the price alarm should be removed from the database$")
public void the_price_alarm_should_be_removed_from_the_database() throws Exception {
}
@Then("^a popup should open where the user can edit the alarm$")
public void a_popup_should_open_where_the_user_can_edit_the_alarm() throws Exception {
}
@Then("^a popup should open where the user can edit the alarm$")
public void a_popup_should_open_where_the_user_can_edit_the_alarm() throws Exception {
}
@When("^the user clicks on the \"([^\"]*)\" button$")
public void the_user_clicks_on_the_button(String arg1) throws Exception {
}
@When("^the user clicks on the \"([^\"]*)\" button$")
public void the_user_clicks_on_the_button(String arg1) throws Exception {
}
@Then("^the price alarm should be updated in the database$")
public void the_price_alarm_should_be_updated_in_the_database() throws Exception {
}
@Then("^the price alarm should be updated in the database$")
public void the_price_alarm_should_be_updated_in_the_database() throws Exception {
}
}

View File

@ -1,52 +1,72 @@
package stepdefs;
import cucumber.api.PendingException;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import io.cucumber.java.PendingException;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class SearchProduct {
@Given("^the user is on the landing page$")
public void the_user_is_on_the_landing_page() throws Exception {
}
@Given("^the user is on the landing page$")
public void the_user_is_on_the_landing_page() throws Exception {
//throw new PendingException();
Preconditions.driver.get("https://betterzon.xyz");
WebElement logo = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo")));
}
@When("^the user enters the search term \"([^\"]*)\" and clicks search$")
public void the_user_enters_the_search_term_and_clicks_search(String arg0) throws Exception {
}
@When("^the user enters the search term \"([^\"]*)\" and clicks search$")
public void the_user_enters_the_search_term_and_clicks_search(String searchTerm) throws Exception {
WebElement searchField = Preconditions.driver.findElement(By.cssSelector(".ng-untouched.ng-pristine.ng-valid"));
searchField.sendKeys(searchTerm);
searchField.sendKeys(Keys.ENTER);
WebElement logo = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".logo")));
}
@Then("^the user should see the error page \"([^\"]*)\"$")
public void the_user_should_see_the_error_page(String arg0) throws Exception {
}
@Then("^the user should see the error page \"([^\"]*)\"$")
public void the_user_should_see_the_error_page(String arg0) throws Exception {
WebElement noProdsFoundMsg = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".ng-star-inserted")));
assert(noProdsFoundMsg.getText().contains("No Products found!"));
}
@Given("^the user is not logged in$")
public void the_user_is_not_logged_in() throws Exception {
}
@Given("^the user is not logged in$")
public void the_user_is_not_logged_in() throws Exception {
}
@Given("^the user is logged in$")
public void the_user_is_logged_in() throws Exception {
}
@Given("^the user is logged in$")
public void the_user_is_logged_in() throws Exception {
}
@Then("^the user should see a list of products$")
public void the_user_should_see_a_list_of_products() throws Exception {
}
@Then("^the user should see a list of products$")
public void the_user_should_see_a_list_of_products() throws Exception {
WebElement product = (new WebDriverWait(Preconditions.driver, 10))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector(".productItem.ng-star-inserted")));
assert(product.isDisplayed());
}
@When("^the user clicks on the first product$")
public void the_user_clicks_on_the_first_product() throws Exception {
}
@When("^the user clicks on the first product$")
public void the_user_clicks_on_the_first_product() throws Exception {
}
@Then("^the user should see the product detail page$")
public void the_user_should_see_the_product_detail_page() throws Exception {
}
@Then("^the user should see the product detail page$")
public void the_user_should_see_the_product_detail_page() throws Exception {
}
@Then("^the set price alarm box should show \"([^\"]*)\"$")
public void the_set_price_alarm_box_should_show(String arg0) throws Exception {
}
@Then("^the set price alarm box should show \"([^\"]*)\"$")
public void the_set_price_alarm_box_should_show(String arg0) throws Exception {
}
@When("^the user sets a price alarm$")
public void the_user_sets_a_price_alarm() throws Exception {
}
@When("^the user sets a price alarm$")
public void the_user_sets_a_price_alarm() throws Exception {
}
@Then("^the user should receive an email confirming the price alarm$")
public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception {
}
@Then("^the user should receive an email confirming the price alarm$")
public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception {
}
}

View File

@ -1,28 +1,28 @@
Feature: Price Alarms
Scenario: Show a list of price alarms
Given the user is on the landing page
And the user is logged in
And the user has at least 1 price alarm set
When the user clicks on the profile icon
Then the profile details popup should open
When the user clicks on price alarms
Then the price alarm list should open
And the price alarm list should contain at least 1 entry
And the price alarm list should contain a maximum of 20 entries per page
Scenario: Show a list of price alarms
Given the user is on the landing page
And the user is logged in
And the user has at least 1 price alarm set
When the user clicks on the profile icon
Then the profile details popup should open
When the user clicks on price alarms
Then the price alarm list should open
And the price alarm list should contain at least 1 entry
And the price alarm list should contain a maximum of 20 entries per page
Scenario: Remove a price alarm
Given the user is on the price alarm list page
And the user is logged in
When the user clicks on the "remove" button next to a price alarm
Then a popup should open asking the user to confirm the removal
When the user confirms the removal of the price alarm
Then the price alarm should be removed from the database
Scenario: Remove a price alarm
Given the user is on the price alarm list page
And the user is logged in
When the user clicks on the "remove" button next to a price alarm
Then a popup should open asking the user to confirm the removal
When the user confirms the removal of the price alarm
Then the price alarm should be removed from the database
Scenario: Edit a price alarm
Given the user is on the price alarm list page
And the user is logged in
When the user clicks on the "edit" button next to a price alarm
Then a popup should open where the user can edit the alarm
When the user clicks on the "save changes" button
Then the price alarm should be updated in the database
Scenario: Edit a price alarm
Given the user is on the price alarm list page
And the user is logged in
When the user clicks on the "edit" button next to a price alarm
Then a popup should open where the user can edit the alarm
When the user clicks on the "save changes" button
Then the price alarm should be updated in the database

View File

@ -1,26 +1,26 @@
Feature: Search a Product
Scenario: User searches for unknown product
Given the user is on the landing page
When the user enters the search term "iPhone 13" and clicks search
Then the user should see the error page "No products found"
Scenario: User searches for unknown product
Given the user is on the landing page
When the user enters the search term "iPhone 13" and clicks search
Then the user should see the error page "No products found"
Scenario: User is not logged in, searches for known product
Given the user is on the landing page
And the user is not logged in
When the user enters the search term "iPhone 12" and clicks search
Then the user should see a list of products
When the user clicks on the first product
Then the user should see the product detail page
And the set price alarm box should show "Log in to continue"
Scenario: User is not logged in, searches for known product
Given the user is on the landing page
And the user is not logged in
When the user enters the search term "iPhone 12" and clicks search
Then the user should see a list of products
When the user clicks on the first product
Then the user should see the product detail page
And the set price alarm box should show "Log in to continue"
Scenario: User is logged in, searches for known product
Given the user is on the landing page
And the user is logged in
When the user enters the search term "iPhone 12" and clicks search
Then the user should see a list of products
When the user clicks on the first product
Then the user should see the product detail page
And the set price alarm box should show "Set price alarm"
When the user sets a price alarm
Then the user should receive an email confirming the price alarm
Scenario: User is logged in, searches for known product
Given the user is on the landing page
And the user is logged in
When the user enters the search term "iPhone 12" and clicks search
Then the user should see a list of products
When the user clicks on the first product
Then the user should see the product detail page
And the set price alarm box should show "Set price alarm"
When the user sets a price alarm
Then the user should receive an email confirming the price alarm

View File

@ -1,18 +1,18 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FooterComponent} from './footer.component';
import {RouterTestingModule} from "@angular/router/testing";
import {AppComponent} from "../../app.component";
import {ImprintComponent} from "../../pages/imprint/imprint.component";
import {ActivatedRoute, Router} from "@angular/router";
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from '../../app.component';
import {ImprintComponent} from '../../pages/imprint/imprint.component';
import {ActivatedRoute, Router} from '@angular/router';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
let router = {
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
}
};
beforeEach(async () => {
await TestBed.configureTestingModule({

View File

@ -1,21 +1,22 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HeaderComponent} from './header.component';
import {RouterTestingModule} from "@angular/router/testing";
import {MatMenuModule} from "@angular/material/menu";
import {Router} from "@angular/router";
import {RouterTestingModule} from '@angular/router/testing';
import {MatMenuModule} from '@angular/material/menu';
import {Router} from '@angular/router';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let router = {
const router = {
navigate: jasmine.createSpy('navigate'),
navigateByUrl: (url: string) => {
return {
then: () => {}
}
then: () => {
}
};
}
}
};
beforeEach(async () => {
await TestBed.configureTestingModule({

View File

@ -1,18 +1,19 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NewestPricesListComponent} from './newest-prices-list.component';
import {RouterTestingModule} from "@angular/router/testing";
import {HttpClient} from "@angular/common/http";
import {AbstractMockObservableService} from "../../mocks/mock.service";
import {ApiService} from "../../services/api.service";
import {RouterTestingModule} from '@angular/router/testing';
import {HttpClient} from '@angular/common/http';
import {AbstractMockObservableService} from '../../mocks/mock.service';
import {ApiService} from '../../services/api.service';
import {Observable} from 'rxjs';
class MockApiService extends AbstractMockObservableService {
getCurrentPricePerVendor() {
getCurrentPricePerVendor(): any {
this.content = [];
return this;
}
getVendors() {
getVendors(): any {
const vendor = {
vendor_id: 1,
name: 'Max Mustermann',

View File

@ -1,18 +1,33 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProductDetailsComponent} from './product-details.component';
import {RouterTestingModule} from "@angular/router/testing";
import {AbstractMockObservableService} from "../../mocks/mock.service";
import {ApiService} from "../../services/api.service";
import {ChartComponent, NgApexchartsModule} from "ng-apexcharts";
import {RouterTestingModule} from '@angular/router/testing';
import {AbstractMockObservableService} from '../../mocks/mock.service';
import {ApiService} from '../../services/api.service';
import {ChartComponent, NgApexchartsModule} from 'ng-apexcharts';
import {By} from '@angular/platform-browser';
class MockApiService extends AbstractMockObservableService {
getProduct() {
this.content = {};
getProduct(): any {
const product = {
product_id: 1,
asin: 'ASIN',
is_active: true,
name: 'Super tolles Produkt',
short_description: 'Descr',
long_description: 'Descr',
image_guid: '123',
date_added: new Date(),
last_modified: new Date(),
manufacturer_id: 1,
selling_rank: '1',
category_id: 1
};
this.content = product;
return this;
}
getLowestPrices() {
getLowestPrices(): any {
const price = {
price_id: 1,
product_id: 1,
@ -24,12 +39,12 @@ class MockApiService extends AbstractMockObservableService {
return this;
}
getAmazonPrice() {
getAmazonPrice(): any {
this.content = {};
return this;
}
getVendors() {
getVendors(): any {
const vendor = {
vendor_id: 1,
name: 'Max Mustermann',
@ -39,7 +54,7 @@ class MockApiService extends AbstractMockObservableService {
country_code: 'DE',
phone: '+49 123 4567890',
website: 'https://www.amazon.de',
}
};
this.content = [vendor];
return this;
}
@ -72,4 +87,10 @@ describe('ProductDetailsComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show the product name', () => {
component.ngOnInit();
const title = fixture.debugElement.query(By.css('.productTitle'));
expect(title.nativeElement.innerText).toEqual('Super tolles Produkt');
});
});

View File

@ -1,20 +1,20 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProductListComponent} from './product-list.component';
import {FooterComponent} from "../footer/footer.component";
import {HeaderComponent} from "../header/header.component";
import {RouterTestingModule} from "@angular/router/testing";
import {ApiService} from "../../services/api.service";
import {AbstractMockObservableService} from "../../mocks/mock.service";
import {Router} from "@angular/router";
import {FooterComponent} from '../footer/footer.component';
import {HeaderComponent} from '../header/header.component';
import {RouterTestingModule} from '@angular/router/testing';
import {ApiService} from '../../services/api.service';
import {AbstractMockObservableService} from '../../mocks/mock.service';
import {Router} from '@angular/router';
class MockApiService extends AbstractMockObservableService {
getProducts() {
getProducts(): any {
this.content = [];
return this;
}
getProductsByQuery() {
getProductsByQuery(): any {
this.content = [];
return this;
}
@ -24,10 +24,10 @@ describe('ProductListComponent', () => {
let component: ProductListComponent;
let fixture: ComponentFixture<ProductListComponent>;
let mockService;
let router = {
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
}
};
beforeEach(async () => {
mockService = new MockApiService();
@ -71,7 +71,7 @@ describe('ProductListComponent', () => {
manufacturer_id: 1,
selling_rank: '1',
category_id: 1
}
};
component.clickedProduct(product);
expect(router.navigate).toHaveBeenCalledWith(['/product/1']);

View File

@ -1,16 +1,16 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {LandingpageComponent} from './landingpage.component';
import {RouterTestingModule} from "@angular/router/testing";
import {Router} from "@angular/router";
import {RouterTestingModule} from '@angular/router/testing';
import {Router} from '@angular/router';
describe('LandingpageComponent', () => {
let component: LandingpageComponent;
let fixture: ComponentFixture<LandingpageComponent>;
let router = {
const router = {
navigate: jasmine.createSpy('navigate'),
routerState: jasmine.createSpy('routerState')
}
};
beforeEach(async () => {
await TestBed.configureTestingModule({

View File

@ -1,7 +1,7 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProductDetailPageComponent} from './product-detail-page.component';
import {RouterTestingModule} from "@angular/router/testing";
import {RouterTestingModule} from '@angular/router/testing';
describe('ProductDetailPageComponent', () => {
let component: ProductDetailPageComponent;

View File

@ -1,10 +1,10 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ProductSearchPageComponent} from './product-search-page.component';
import {HeaderComponent} from "../../components/header/header.component";
import {FooterComponent} from "../../components/footer/footer.component";
import {ProductListComponent} from "../../components/product-list/product-list.component";
import {RouterTestingModule} from "@angular/router/testing";
import {HeaderComponent} from '../../components/header/header.component';
import {FooterComponent} from '../../components/footer/footer.component';
import {ProductListComponent} from '../../components/product-list/product-list.component';
import {RouterTestingModule} from '@angular/router/testing';
describe('ProductSearchPageComponent', () => {
let component: ProductSearchPageComponent;

View File

@ -3,9 +3,9 @@ Website: https://www.betterzon.xyz<br>
Blog: https://blog.betterzon.xyz<br>
Wiki: https://github.com/Mueller-Patrick/Betterzon/wiki
# Code Quality
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/88e47ebf837b43af9d12147c22f77f7f)](https://www.codacy.com/gh/Mueller-Patrick/Betterzon/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=Mueller-Patrick/Betterzon&amp;utm_campaign=Badge_Grade)
# Project Status
![Latest Commit Build Status](https://ci.betterzon.xyz/job/Verify_Build_on_PR/badge/icon?style=flat-square&subject=Latest%20Commit)
![Deployment Status](https://ci.betterzon.xyz/job/GitHub%20Deployment/badge/icon?style=flat-square&subject=Deployment&status=Success)
<br>
[![Website Status](https://img.shields.io/website?label=www.betterzon.xyz&style=for-the-badge&url=https%3A%2F%2Fwww.betterzon.xyz)](https://www.betterzon.xyz)
[![Blog Status](https://img.shields.io/website?label=blog.betterzon.xyz&style=for-the-badge&url=https%3A%2F%2Fblog.betterzon.xyz)](https://blog.betterzon.xyz)