diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..d41a639 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,7 @@ +pytest: + stage: test + script: + - python -m venv .venv + - source ./.venv/scripts/activate + - pip install -r requirements.txt + - pytest \ No newline at end of file diff --git a/README.md b/README.md index 991943a..fcc19a4 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,13 @@ python -m venv .venv pip install -r requirements.txt ``` +## Testing + +To run the full suite of unit tests for the webapp simply run the following command in the venv +```sh +pytest +``` + ## Running ### Pre-Requisites diff --git a/controllers/database/database.py b/controllers/database/database.py index 69a120c..b43e1b1 100644 --- a/controllers/database/database.py +++ b/controllers/database/database.py @@ -1,14 +1,25 @@ from abc import ABC, abstractmethod from typing import Mapping, Any import sqlite3 +import os class DatabaseController(ABC): - __sqlitefile = "./data/wmgzon.db" + __data_dir = "./data/" + __db_name = "wmgzon.db" + + # Use test file if necessary + if os.environ.get("ENVIRON") == "test": + __db_name = "test_" + __db_name + + __sqlitefile = __data_dir + __db_name + def __init__(self): self._conn = None try: - self._conn = sqlite3.connect(self.__sqlitefile) + # Creates a connection and specifies a flag to parse all types back down into + # Python declared types e.g. date & time + self._conn = sqlite3.connect(self.__sqlitefile, detect_types=sqlite3.PARSE_DECLTYPES) except sqlite3.Error as e: # Close the connection if still open if self._conn: diff --git a/controllers/database/product.py b/controllers/database/product.py index 81cd436..8a8c97e 100644 --- a/controllers/database/product.py +++ b/controllers/database/product.py @@ -2,7 +2,7 @@ from .database import DatabaseController from models.products.product import Product class ProductController(DatabaseController): - FIELDS = ['id', 'name', 'image', 'description', 'cost', 'category', 'sellerID', 'postedDate', 'quantity'] + FIELDS = ['id', 'name', 'image', 'description', 'cost', 'category', 'sellerID', 'postedDate', 'quantityAvailable'] def __init__(self): super().__init__() @@ -20,7 +20,7 @@ class ProductController(DatabaseController): ] self._conn.execute( - "INSERT INTO Products (name, cost, image, description, category, sellerID, postedDate, quantityAvailable) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + "INSERT INTO Products (name, image, description, cost, categoryID, sellerID, postedDate, quantityAvailable) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", params ) self._conn.commit() @@ -46,7 +46,7 @@ class ProductController(DatabaseController): for product in rows: params = dict(zip(self.FIELDS, product)) obj = self.new_instance(Product, params) - products.push(obj) + products.append(obj) return products diff --git a/controllers/web/user.py b/controllers/web/user.py index 0c5458e..a872cab 100644 --- a/controllers/web/user.py +++ b/controllers/web/user.py @@ -55,14 +55,12 @@ def signup(): return redirect("/signup") database.create(Customer( - 0, request.form['username'], sha512(request.form['password'].encode()).hexdigest(), # Hashed as soon as it is recieved on the backend request.form['firstname'], request.form['lastname'], request.form['email'], - "123", - "Customer" + "123" )) # Code 307 Preserves the original request (POST) diff --git a/docker-compose.yml b/docker-compose.yml index dd084ea..98d9fda 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,9 @@ services: build: . environment: - APPSECRET=test + - ENVIRON=test + # - ENVIRON=prod + tty: true ports: - "5000:5000" volumes: diff --git a/dockerfile b/dockerfile index df68b1d..2b9c37f 100644 --- a/dockerfile +++ b/dockerfile @@ -3,5 +3,5 @@ COPY ./requirements.txt /app/requirements.txt WORKDIR /app RUN pip install -r requirements.txt COPY . /app -RUN chmod +x scripts/run.sh -CMD ["sh", "scripts/run.sh"] \ No newline at end of file +RUN chmod +x scripts/run.bash +CMD ["bash", "scripts/run.bash"] \ No newline at end of file diff --git a/models/products/product.py b/models/products/product.py index 34aaea8..ed65af6 100644 --- a/models/products/product.py +++ b/models/products/product.py @@ -10,8 +10,25 @@ class Product: self.image = "/static/assets/wmgzon.png" self.description = "" self.cost = 0.0 - self.category = "" + self.category = 0 self.sellerID = 0 self.postedDate = datetime.now() self.quantityAvailable = 0 + + ''' + Class constructor to instatiate a customer object + + No additional properties are assigned to the customer + ''' + def __init__(self, name: str, image: str, description: str, cost: float, category: int, + seller_id: int, posted_date: datetime, quantity_available: int): + self.id = 0 + self.name = name + self.image = image + self.description = description + self.cost = cost + self.category = category + self.sellerID = seller_id + self.postedDate = posted_date + self.quantityAvailable = quantity_available \ No newline at end of file diff --git a/models/users/admin.py b/models/users/admin.py index 917907f..946a53e 100644 --- a/models/users/admin.py +++ b/models/users/admin.py @@ -6,8 +6,8 @@ class Admin(User): No additional properties are assigned to the admin ''' - def __init__(self, id: int, username: str, password: str, firstname: str, - lastname: str, email: str, phone: str, role: str): + def __init__(self, username: str, password: str, firstname: str, + lastname: str, email: str, phone: str): super().__init__( - id, username, password, firstname, lastname, email, phone, role + username, password, firstname, lastname, email, phone, "Admin" ) diff --git a/models/users/customer.py b/models/users/customer.py index 34a5e64..0c571ad 100644 --- a/models/users/customer.py +++ b/models/users/customer.py @@ -6,9 +6,9 @@ class Customer(User): No additional properties are assigned to the customer ''' - def __init__(self, id: int, username: str, password: str, firstname: str, - lastname: str, email: str, phone: str, role: str): + def __init__(self, username: str, password: str, firstname: str, + lastname: str, email: str, phone: str): super().__init__( - id, username, password, firstname, lastname, email, phone, role + username, password, firstname, lastname, email, phone, "Customer" ) diff --git a/models/users/seller.py b/models/users/seller.py index 57473ac..28bb3b1 100644 --- a/models/users/seller.py +++ b/models/users/seller.py @@ -6,9 +6,9 @@ class Seller(User): No additional properties are assigned to the customer ''' - def __init__(self, id: int, username: str, password: str, firstname: str, - lastname: str, email: str, phone: str, role: str): + def __init__(self, username: str, password: str, firstname: str, + lastname: str, email: str, phone: str): super().__init__( - id, username, password, firstname, lastname, email, phone, role + id, username, password, firstname, lastname, email, phone, "Seller" ) self.store = "" diff --git a/models/users/user.py b/models/users/user.py index e282500..95d3206 100644 --- a/models/users/user.py +++ b/models/users/user.py @@ -3,9 +3,9 @@ from abc import ABC class User(ABC): """ Functional Class constructor to initialise all properties in the base object with a value """ - def __init__(self, id: int, username: str, password: str, firstname: str, + def __init__(self, username: str, password: str, firstname: str, lastname: str, email: str, phone: str, role: str): - self.id = id + self.id = 0 self.username = username self.password = password self.firstName = firstname diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/create_database.py b/scripts/create_database.py index adcc2b9..442c5d5 100644 --- a/scripts/create_database.py +++ b/scripts/create_database.py @@ -16,9 +16,14 @@ def create_connection(path: str, filename: str): # Execute creation scripts sql = open("scripts/create_tables.sql", "r"); conn.executescript(sql.read()) - - print("SQLite Version: " + sqlite3.version) + print("Table creation complete") + + # Populate with test data if we are in Test Mode + if os.environ.get("ENVIRON") == "test": + sql = open("scripts/test_data.sql", "r"); + conn.executescript(sql.read()) + except sqlite3.Error as e: print(e) finally: @@ -32,5 +37,22 @@ def create_directory(dir: str): except FileExistsError: pass -if __name__ == '__main__': - create_connection(r"./data/", r"wmgzon.db") +def remove_file(dir: str): + try: + os.remove(dir) + except FileNotFoundError: + pass + + + +dir = r"./data/" +db_name = r"wmgzon.db" + +# Check for test environ +if os.environ.get("ENVIRON") == "test": + # Remove the original test database + print("TEST ENVIRONMENT ACTIVE") + db_name = "test_" + db_name + remove_file(dir + db_name) + +create_connection(dir, db_name) diff --git a/scripts/create_tables.sql b/scripts/create_tables.sql index 5a1be62..5a582c2 100644 --- a/scripts/create_tables.sql +++ b/scripts/create_tables.sql @@ -9,8 +9,6 @@ CREATE TABLE IF NOT EXISTS Users ( role TEXT NOT NULL ); -INSERT INTO Users (first_name, last_name, username, email, phone, password, role) VALUES ("Luke", "Else", "lukejelse04", "test@test.com", "07498 289321", "test213", "Customer"); - CREATE TABLE IF NOT EXISTS Categories ( id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE @@ -23,9 +21,6 @@ INSERT INTO Categories (name) VALUES ("Books"); INSERT INTO Categories (name) VALUES ("Phones"); INSERT INTO Categories (name) VALUES ("Music"); - - - CREATE TABLE IF NOT EXISTS Products ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -39,21 +34,11 @@ CREATE TABLE IF NOT EXISTS Products ( categoryID INTEGER NOT NULL REFERENCES Categories (id) ON DELETE CASCADE - ON UPDATE NO ACTION + ON UPDATE NO ACTION, + postedDate TIMESTAMP, + quantityAvailable INTEGER DEFAULT 0 ); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 2, 2); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 3, 3); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 3, 3); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 3, 3); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 6, 6); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 6, 6); -INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 5, 5); - CREATE TABLE IF NOT EXISTS Orders ( id INTEGER PRIMARY KEY, sellerID TEXT NOT NULL diff --git a/scripts/run.bash b/scripts/run.bash new file mode 100644 index 0000000..7d52bd5 --- /dev/null +++ b/scripts/run.bash @@ -0,0 +1,2 @@ +#! /bin/bash +pytest --disable-warnings && python ./scripts/create_database.py && python ./app.py \ No newline at end of file diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100644 index e9019d0..0000000 --- a/scripts/run.sh +++ /dev/null @@ -1,4 +0,0 @@ -#! /bin/sh - -python ./scripts/create_database.py -python ./app.py \ No newline at end of file diff --git a/scripts/test_data.sql b/scripts/test_data.sql new file mode 100644 index 0000000..31cc24a --- /dev/null +++ b/scripts/test_data.sql @@ -0,0 +1,37 @@ +INSERT INTO Users (first_name, last_name, username, email, phone, password, role) VALUES ("Luke", "Else", "lukejelse04", "test@test.com", "07498 289321", "test213", "Customer"); + +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 2); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 2); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 3); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 6); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 6); + +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 2); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 2); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 3); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 6); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 6); + +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 1); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 2); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 2); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 3); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 4); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 6); +INSERT INTO Products (name, image, description, cost, sellerID, categoryID) VALUES ("test", "assets/img/wmgzon.png", "this is a product", 20.99, 1, 6); \ No newline at end of file diff --git a/tests/database/__init__.py b/tests/database/__init__.py new file mode 100644 index 0000000..3c3cf69 --- /dev/null +++ b/tests/database/__init__.py @@ -0,0 +1,8 @@ +# Ensure test environment is set before using +import os + +# Setup test environment variables +os.environ["ENVIRON"] = "test" + +# Runs the database creation scripts +import scripts.create_database \ No newline at end of file diff --git a/tests/database/test_products.py b/tests/database/test_products.py new file mode 100644 index 0000000..84c8ab2 --- /dev/null +++ b/tests/database/test_products.py @@ -0,0 +1,40 @@ +import pytest +import sqlite3 +from datetime import datetime +from controllers.database.product import ProductController +from models.products.product import Product + +product = Product( + "product", + "image.png", + "description", + 10.00, + 1, + 1, + datetime.now(), + 1 +) + +# Tests a new product can be created +def test_create_product(): + db = ProductController() + db.create(product) + +# Tests the database maintains integrity when we try and add a product with the same details +def test_duplicate_product(): + db = ProductController() + with pytest.raises(sqlite3.IntegrityError): + db.create(product) + +# Test we the same product details get returned from the database +def test_read_product(): + db = ProductController() + + # Test the same product is returned + new_product = db.read("product") + assert isinstance(new_product, list) + assert isinstance(new_product[0], Product) + + # Update the ID on the item as database assigns new id + product.id = new_product[0].id + assert new_product[0].__dict__ == product.__dict__ diff --git a/tests/database/test_users.py b/tests/database/test_users.py new file mode 100644 index 0000000..95281f8 --- /dev/null +++ b/tests/database/test_users.py @@ -0,0 +1,36 @@ +import pytest +import sqlite3 +from controllers.database.user import UserController +from models.users.customer import Customer + +customer = Customer( + "testuser", + "Password1", + "firstname", + "lastname", + "test@test", + "123456789" +) + +# Tests a new user can be created +def test_create_user(): + db = UserController() + db.create(customer) + +# Tests the database maintains integrity when we try and add a user with the same details +def test_duplicate_user(): + db = UserController() + with pytest.raises(sqlite3.IntegrityError): + db.create(customer) + +# Test we the same user details get returned from the database +def test_read_user(): + db = UserController() + + # Test the same user is returned + user = db.read("testuser") + assert isinstance(user, Customer) + + # Update the ID on the item as database assigns new id + customer.id = user.id + assert user.__dict__ == customer.__dict__