Compare commits

...

2 Commits

Author SHA1 Message Date
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
22 changed files with 5263 additions and 252 deletions

4984
Backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -16,6 +16,8 @@ import {notFoundHandler} from './middleware/notFound.middleware';
import {usersRouter} from './models/users/users.router'; import {usersRouter} from './models/users/users.router';
import {pricealarmsRouter} from './models/pricealarms/pricealarms.router'; import {pricealarmsRouter} from './models/pricealarms/pricealarms.router';
const cookieParser = require('cookie-parser');
dotenv.config(); dotenv.config();
@ -39,6 +41,7 @@ const app = express();
app.use(helmet()); app.use(helmet());
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use(cookieParser());
app.use('/products', productsRouter); app.use('/products', productsRouter);
app.use('/categories', categoriesRouter); app.use('/categories', categoriesRouter);
app.use('/manufacturers', manufacturersRouter); app.use('/manufacturers', manufacturersRouter);

View File

@ -1,5 +1,5 @@
import HttpException from "../common/http-exception"; import HttpException from '../common/http-exception';
import { Request, Response, NextFunction } from "express"; import {Request, Response, NextFunction} from 'express';
export const errorHandler = ( export const errorHandler = (
error: HttpException, error: HttpException,
@ -9,7 +9,7 @@ export const errorHandler = (
) => { ) => {
const status = error.statusCode || 500; const status = error.statusCode || 500;
const message = 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); 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 = ( export const notFoundHandler = (
request: Request, request: Request,
@ -6,7 +6,7 @@ export const notFoundHandler = (
next: NextFunction next: NextFunction
) => { ) => {
const message = "Resource not found"; const message = 'Resource not found';
response.status(404).send(message); response.status(404).send(message);
}; };

View File

@ -27,7 +27,7 @@ categoriesRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(categories); res.status(200).send(categories);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(category);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(categories);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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

@ -27,7 +27,7 @@ manufacturersRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(manufacturers); res.status(200).send(manufacturers);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 @@ manufacturersRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(manufacturer); res.status(200).send(manufacturer);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 @@ manufacturersRouter.get('/search/:term', async (req: Request, res: Response) =>
res.status(200).send(manufacturer); res.status(200).send(manufacturer);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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,17 +23,8 @@ export const pricealarmsRouter = express.Router();
pricealarmsRouter.get('/', async (req: Request, res: Response) => { pricealarmsRouter.get('/', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip);
if (!session_id || !session_key) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
const user = await UserService.checkSession(session_id, session_key, user_ip);
const priceAlarms = await PriceAlarmsService.getPriceAlarms(user.user_id); const priceAlarms = await PriceAlarmsService.getPriceAlarms(user.user_id);
@ -48,17 +39,8 @@ pricealarmsRouter.get('/', async (req: Request, res: Response) => {
pricealarmsRouter.post('/create', async (req: Request, res: Response) => { pricealarmsRouter.post('/create', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip);
if (!session_id || !session_key) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get info for price alarm creation // Get info for price alarm creation
const product_id = req.body.product_id; const product_id = req.body.product_id;
@ -90,17 +72,8 @@ pricealarmsRouter.post('/create', async (req: Request, res: Response) => {
pricealarmsRouter.put('/update', async (req: Request, res: Response) => { pricealarmsRouter.put('/update', async (req: Request, res: Response) => {
try { try {
// Authenticate user // Authenticate user
const session_id = req.body.session_id;
const session_key = req.body.session_key;
const user_ip = req.connection.remoteAddress ?? ''; const user_ip = req.connection.remoteAddress ?? '';
const user = await UserService.checkSessionWithCookie(req.cookies.betterauth, user_ip);
if (!session_id || !session_key) {
// Missing
res.status(400).send(JSON.stringify({message: 'Missing parameters'}));
return;
}
const user = await UserService.checkSession(session_id, session_key, user_ip);
// Get info for price alarm creation // Get info for price alarm creation
const alarm_id = req.body.alarm_id; const alarm_id = req.body.alarm_id;

View File

@ -40,7 +40,7 @@ pricesRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(prices); res.status(200).send(prices);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(price);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(prices);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(prices);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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

@ -27,7 +27,7 @@ productsRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(products); res.status(200).send(products);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(product);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(products);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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); res.status(200).send(products);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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

@ -47,10 +47,13 @@ usersRouter.post('/register', async (req: Request, res: Response) => {
const session: Session = await UserService.createUser(username, password, email, ip); const session: Session = await UserService.createUser(username, password, email, ip);
// Send the session details back to the user // 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) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 // Update the user entry and create a session
const session: Session = await UserService.login(username, password, ip); const session: Session = await UserService.login(username, password, ip);
if(!session.session_id) { if (!session.session_id) {
// Error logging in, probably wrong username / password // 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; return;
} }
// Send the session details back to the user // 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) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 // POST users/checkSessionValid
usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => { usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
try { try {
const sessionId: string = req.body.sessionId;
const sessionKey: string = req.body.sessionKey;
const ip: string = req.connection.remoteAddress ?? ''; 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 // 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 // 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; return;
} }
@ -110,6 +108,6 @@ usersRouter.post('/checkSessionValid', async (req: Request, res: Response) => {
res.status(201).send(user); res.status(201).send(user);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 { return {
session_id: sessionId, session_id: sessionId,
session_key: sessionKey, session_key: sessionKey,
session_key_hash: '', session_key_hash: 'HIDDEN',
last_IP: ip last_IP: ip
}; };
@ -135,7 +135,7 @@ export const login = async (username: string, password: string, ip: string): Pro
return { return {
session_id: sessionId, session_id: sessionId,
session_key: sessionKey, session_key: sessionKey,
session_key_hash: '', session_key_hash: 'HIDDEN',
last_IP: ip last_IP: ip
}; };
@ -179,7 +179,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
// Key is valid, continue // Key is valid, continue
// Check if the session is still valid // Check if the session is still valid
if(validUntil <= new Date()) { if (validUntil <= new Date()) {
// Session expired, return invalid // Session expired, return invalid
return {} as User; return {} as User;
} }
@ -193,7 +193,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
await conn.commit(); await conn.commit();
// Get the other required user information and update the user // 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); const userRows = await conn.query(userQuery, userId);
let username = ''; let username = '';
let email = ''; let email = '';
@ -213,7 +213,7 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
user_id: userId, user_id: userId,
username: username, username: username,
email: email, email: email,
password_hash: '', password_hash: 'HIDDEN',
registration_date: registrationDate, registration_date: registrationDate,
last_login_date: lastLoginDate last_login_date: lastLoginDate
}; };
@ -229,6 +229,20 @@ export const checkSession = async (sessionId: string, sessionKey: string, ip: st
return {} as User; 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 * Used in the checkUsernameAndEmail method as return value
*/ */

View File

@ -27,7 +27,7 @@ vendorsRouter.get('/', async (req: Request, res: Response) => {
res.status(200).send(vendors); res.status(200).send(vendors);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 @@ vendorsRouter.get('/:id', async (req: Request, res: Response) => {
res.status(200).send(vendor); res.status(200).send(vendor);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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 @@ vendorsRouter.get('/search/:term', async (req: Request, res: Response) => {
res.status(200).send(vendors); res.status(200).send(vendors);
} catch (e) { } catch (e) {
console.log('Error handling a request: ' + e.message); 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

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

View File

@ -10,17 +10,24 @@
<sourceFolder url="file://$MODULE_DIR$/src/test/resource" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/test/resource" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="jdk" jdkName="openjdk-16" jdkType="JavaSDK" />
<orderEntry type="sourceFolder" forTests="false" /> <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: junit:junit:4.12" level="project" />
<orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" 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.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-plugin-api:3.0" level="project" />
<orderEntry type="library" name="Maven: org.apache.maven:maven-model: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-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" 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" 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> </component>
</module> </module>

View File

@ -13,21 +13,30 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>io.cucumber</groupId> <groupId>junit</groupId>
<artifactId>cucumber-java</artifactId> <artifactId>junit</artifactId>
<version>2.3.1</version> <version>4.12</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>6.10.3</version>
</dependency>
<dependency> <dependency>
<groupId>io.cucumber</groupId> <groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId> <artifactId>cucumber-junit</artifactId>
<version>2.3.1</version> <version>6.10.3</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version> <version>3.8.1</version>
</dependency> </dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.141.59</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,6 +1,10 @@
import cucumber.api.CucumberOptions; import io.cucumber.junit.Cucumber;
import cucumber.api.junit.Cucumber; import io.cucumber.junit.CucumberOptions;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.openqa.selenium.firefox.FirefoxDriver;
import stepdefs.Preconditions;
@RunWith(Cucumber.class) @RunWith(Cucumber.class)
@CucumberOptions( @CucumberOptions(
@ -9,4 +13,13 @@ import org.junit.runner.RunWith;
) )
public class RunTest { 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; package stepdefs;
import cucumber.api.java.en.Given; import io.cucumber.java.PendingException;
import cucumber.api.java.en.Then; import io.cucumber.java.en.Given;
import cucumber.api.java.en.When; import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
public class PriceAlarm { public class PriceAlarm {
@Given("^the user has at least (\\d+) price alarm set$") @Given("^the user has at least (\\d+) price alarm set$")
public void the_user_has_at_least_price_alarm_set(int arg1) throws Exception { public void the_user_has_at_least_price_alarm_set(int arg1) throws Exception {
} }
@When("^the user clicks on the profile icon$") @When("^the user clicks on the profile icon$")
public void the_user_clicks_on_the_profile_icon() throws Exception { public void the_user_clicks_on_the_profile_icon() throws Exception {
} }
@Then("^the profile details popup should open$") @Then("^the profile details popup should open$")
public void the_profile_details_popup_should_open() throws Exception { public void the_profile_details_popup_should_open() throws Exception {
} }
@When("^the user clicks on price alarms$") @When("^the user clicks on price alarms$")
public void the_user_clicks_on_price_alarms() throws Exception { public void the_user_clicks_on_price_alarms() throws Exception {
} }
@Then("^the price alarm list should open$") @Then("^the price alarm list should open$")
public void the_price_alarm_list_should_open() throws Exception { public void the_price_alarm_list_should_open() throws Exception {
} }
@Then("^the price alarm list should contain at least (\\d+) entry$") @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 { 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$") @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 { 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$") @Given("^the user is on the price alarm list page$")
public void the_user_is_on_the_price_alarm_list_page() throws Exception { 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$") @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 { 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$") @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 { 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$") @When("^the user confirms the removal of the price alarm$")
public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception { public void the_user_confirms_the_removal_of_the_price_alarm() throws Exception {
} }
@Then("^the price alarm should be removed from the database$") @Then("^the price alarm should be removed from the database$")
public void the_price_alarm_should_be_removed_from_the_database() throws Exception { 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$") @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 { public void a_popup_should_open_where_the_user_can_edit_the_alarm() throws Exception {
} }
@When("^the user clicks on the \"([^\"]*)\" button$") @When("^the user clicks on the \"([^\"]*)\" button$")
public void the_user_clicks_on_the_button(String arg1) throws Exception { public void the_user_clicks_on_the_button(String arg1) throws Exception {
} }
@Then("^the price alarm should be updated in the database$") @Then("^the price alarm should be updated in the database$")
public void the_price_alarm_should_be_updated_in_the_database() throws Exception { public void the_price_alarm_should_be_updated_in_the_database() throws Exception {
} }
} }

View File

@ -1,52 +1,72 @@
package stepdefs; package stepdefs;
import cucumber.api.PendingException; import io.cucumber.java.PendingException;
import cucumber.api.java.en.Given; import io.cucumber.java.en.Given;
import cucumber.api.java.en.Then; import io.cucumber.java.en.Then;
import cucumber.api.java.en.When; 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 { public class SearchProduct {
@Given("^the user is on the landing page$") @Given("^the user is on the landing page$")
public void the_user_is_on_the_landing_page() throws Exception { 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$") @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 { 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 \"([^\"]*)\"$") @Then("^the user should see the error page \"([^\"]*)\"$")
public void the_user_should_see_the_error_page(String arg0) throws Exception { 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$") @Given("^the user is not logged in$")
public void the_user_is_not_logged_in() throws Exception { public void the_user_is_not_logged_in() throws Exception {
} }
@Given("^the user is logged in$") @Given("^the user is logged in$")
public void the_user_is_logged_in() throws Exception { public void the_user_is_logged_in() throws Exception {
} }
@Then("^the user should see a list of products$") @Then("^the user should see a list of products$")
public void the_user_should_see_a_list_of_products() throws Exception { 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$") @When("^the user clicks on the first product$")
public void the_user_clicks_on_the_first_product() throws Exception { public void the_user_clicks_on_the_first_product() throws Exception {
} }
@Then("^the user should see the product detail page$") @Then("^the user should see the product detail page$")
public void the_user_should_see_the_product_detail_page() throws Exception { public void the_user_should_see_the_product_detail_page() throws Exception {
} }
@Then("^the set price alarm box should show \"([^\"]*)\"$") @Then("^the set price alarm box should show \"([^\"]*)\"$")
public void the_set_price_alarm_box_should_show(String arg0) throws Exception { public void the_set_price_alarm_box_should_show(String arg0) throws Exception {
} }
@When("^the user sets a price alarm$") @When("^the user sets a price alarm$")
public void the_user_sets_a_price_alarm() throws Exception { public void the_user_sets_a_price_alarm() throws Exception {
} }
@Then("^the user should receive an email confirming the price alarm$") @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 { public void the_user_should_receive_an_email_confirming_the_price_alarm() throws Exception {
} }
} }

View File

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

View File

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