diff --git a/.github/workflows/ms5yml b/.github/workflows/ms5yml index d352c7f..6dcea33 100644 --- a/.github/workflows/ms5yml +++ b/.github/workflows/ms5yml @@ -1,59 +1,41 @@ -name: ms5API Testing +name: API CI/CD -on: - push: #we want only the milstone5 - branches: [Milestone-5_feature_branch] - pull_request: - branches: [Milestone-5_feature_branch] +on: + push: + branches: + - Milestone6-ericfeaturebranch jobs: - api-testing: - runs-on: ubuntu-latest + tests: + runs-on: self-hosted + strategy: + matrix: + #python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8"] steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python 3 - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flask flask_cors flasgger pylint pytest - - # Run pylint - - name: Run pylint - run: | - pylint ./backend/*.py - continue-on-error: false # Set to true to see warnings - - #docker image build - - name: Build API Docker Image - run: | - docker build -t api-image -f Dockerfile . - - #run docker container - - name: Start API Container - run: | - docker run -d -p 5000:5000 --name api-container api-image - - #running main.py - - name: Start API Service - run: | - docker exec api-container python main.py - - #test_main.py - - name: Run test_main.py - run: | - docker exec api-container pytest test_main.py --maxfail=1 --disable-warnings - - #cut the docker - - name: Stop and Remove API Container - run: | - docker stop api-container - docker rm api-container - - + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install pylint + run: | + python -m pip install --upgrade pip + pip install pylint + + - name: Install pytest + run: | + python -m pip install --upgrade pip + pip install pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run Api + run: | + python3 Backend/app.py & + + - name: pytest + run: | + pytest + diff --git a/Backend/Dockerfile b/Backend/Dockerfile deleted file mode 100644 index d085169..0000000 --- a/Backend/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# Use Python as the base image -FROM python:3.8-slim-buster - -# Install pip and Python dependencies -RUN apt-get update && apt-get install -y python3 python3-pip - -# Keeps Python from generating .pyc files in the container -ENV PYTHONDONTWRITEBYTECODE=1 - -# Turns off buffering for easier container logging -ENV PYTHONUNBUFFERED=1 - - -# Set the working directory in the container -WORKDIR /app - - -# Install pip requirements -COPY /requirements.txt . -RUN python3 -m pip install -r requirements.txt - -# Copy main application file -COPY generateData.py . - -# Set the port for the application -ENV PORT=5000 -EXPOSE 5000 - -# Command to run the application -CMD ["python3", "generateData.py"] \ No newline at end of file diff --git a/Backend/main.py b/Backend/app.py similarity index 59% rename from Backend/main.py rename to Backend/app.py index 913cd46..a24b08b 100644 --- a/Backend/main.py +++ b/Backend/app.py @@ -2,11 +2,8 @@ This module provides functions for HTTP communication and route functions """ -import sqlite3 -import databasefunctions from flask import Flask, request, jsonify -from petfuc import get_random_pet -from userfuc import add_user_faviorte, remove_user_faviorte, replace_user_location +from database_files import databasefunctions app = Flask(__name__) # ----------------------------------------------------------------------------------------- @@ -17,9 +14,11 @@ def add_newuser(): Adds new user to database """ data = request.get_json() - newuser_id = databasefunctions.insert_user(data.get("name"), data.get("age"), data.get("location")) + newuser_id = databasefunctions.insert_user(data.get("username"),data.get("password"), + data.get("name"), data.get("age"), + data.get("location")) - return jsonify({'user_id': newuser_id}), 200 + return jsonify({'user_id': newuser_id}), 200 @app.route("/add_newpet", methods=["POST"]) def add_newpet(): @@ -28,21 +27,29 @@ def add_newpet(): """ data = request.get_json() newpet_id = databasefunctions.insert_pet(data.get("name"), data.get("age"), - data.get("gender"), data.get("breed"), data.get("type"), + data.get("gender"), data.get("breed"), data.get("type"), data.get("location"), data.get("photo_path")) - return jsonify({'pet_id': newpet_id}), 200 + return jsonify({'pet_id': newpet_id}), 200 @app.route("/add_newfavorite", methods=["POST"]) def add_newfavorite(): """ Adds new user favorite to database + INSERT INTO pets (name, age, gender, breed, type, location, photo_path) + VALUES + ('Bella', 3, 'female', 'Golden Retriever', 'dog', 'New York', 'images/Chuck.jpg'), + ('Max', 5, 'male', 'Maine Coon', 'cat', 'Boston', 'images/gunner.jpg'), + ('Luna', 2, 'female', 'Siamese', 'cat', 'Chicago', 'images/luna.jpg'), + ('Charlie', 4, 'male', 'Beagle', 'dog', 'Los Angeles', 'images/lizzie_izzie.jpg'), + ('Daisy', 1, 'female', 'Labrador Retriever', 'dog', 'Miami', 'images/monster.jpg'); + """ data = request.get_json() newfavorite_id = databasefunctions.insert_favorite(data.get("user_id"), data.get("pet_id")) - return jsonify({'faviorte_id': newfavorite_id}), 200 + return jsonify({'faviorte_id': newfavorite_id}), 200 @@ -66,7 +73,7 @@ def removepet(): Remove pet from database """ data = request.get_json() - petid = data.get("petid") + petid = data.get("pet_id") roweffected = databasefunctions.remove_pet(petid) return (f"{roweffected}, row was deleted in pet table"), 200 @@ -85,6 +92,25 @@ def removefavorite(): # --------------------------------------------------------------------- + +@app.route("/fetch_userid", methods=["POST"]) +def fetch_userid(): + """ + fetch userid associated with username and password + """ + data = request.get_json() + username = data.get("username") + password = data.get("password") + + + userid = databasefunctions.fetch_userid_from_username_and_password(username, password) + + return jsonify(userid), 200 + + + + + @app.route("/fetch_pet", methods=["POST"]) def fetch_pet(): """ @@ -94,7 +120,7 @@ def fetch_pet(): petid = data.get("pet_id") users_row = databasefunctions.fetch_pet_by_id(petid) - return jsonify(users_row), 200 + return jsonify(users_row), 200 @app.route("/fetch_user", methods=["POST"]) @@ -106,7 +132,7 @@ def fetch_user(): userid = data.get("user_id") users_row = databasefunctions.fetch_user_by_id(userid) - return jsonify(users_row), 200 + return jsonify(users_row), 200 @app.route("/fetch_favorited_pets", methods=["POST"]) @@ -118,11 +144,35 @@ def fetch_favorite_pet(): userid = data.get("user_id") favorited_pets = databasefunctions.fetch_favorites_by_user(userid) - return jsonify(favorited_pets), 200 + return jsonify(favorited_pets), 200 # --------------------------------------------------------------- +@app.route("/fetch_allusers", methods=["POST"]) +def fetch_allusers(): + """ + fetches all users in database + """ + + allusers = databasefunctions.fetch_all_users() + return jsonify(allusers), 200 + + +@app.route("/fetch_allpets", methods=["POST"]) +def fetch_allpets(): + """ + fetches all pets in database + """ + + allpets = databasefunctions.fetch_all_pets() + return jsonify(allpets), 200 + + + + + + if __name__ == "__main__": - app.run(host="0.0.0.0", port=5000, debug=True) + app.run(host="0.0.0.0", debug=True) diff --git a/Backend/database_files/__pycache__/databasefunctions.cpython-310.pyc b/Backend/database_files/__pycache__/databasefunctions.cpython-310.pyc new file mode 100644 index 0000000..d1bdb58 Binary files /dev/null and b/Backend/database_files/__pycache__/databasefunctions.cpython-310.pyc differ diff --git a/Backend/databasefunctions.py b/Backend/database_files/databasefunctions.py similarity index 79% rename from Backend/databasefunctions.py rename to Backend/database_files/databasefunctions.py index f0cecf4..4aa835e 100644 --- a/Backend/databasefunctions.py +++ b/Backend/database_files/databasefunctions.py @@ -1,20 +1,21 @@ +'''DATABASE FUNCTIONS''' from contextlib import closing import sqlite3 def connect_db(): """Establish a connection to the SQLite database.""" - return sqlite3.connect('Projectdatabase.db') #this might change + return sqlite3.connect('projectdatabase.db') #this might change -# ---------------------------------------------------------------------------------------------- insert functions +# --------------------- insert functions -def insert_user(name, age, location): +def insert_user(username, password, name, age, location): """Insert a new user into the accounts table.""" with closing(connect_db()) as conn, conn: cursor = conn.cursor() cursor.execute( - '''INSERT INTO accounts (name, age, location) - VALUES (?, ?, ?)''', - (name, age, location) + '''INSERT INTO accounts (username, password, name, age, location) + VALUES (?, ?, ?, ?, ?)''', + (username, password, name, age, location) ) conn.commit() return cursor.lastrowid @@ -45,7 +46,7 @@ def insert_favorite(user_id, pet_id): conn.commit() return cursor.lastrowid -# ------------------------------------------------------------------------------------- remove functions +# ---------------------------remove functions def remove_user(user_id): """Remove a pet from the accounts table.""" @@ -77,79 +78,99 @@ def remove_favorite(user_id, pet_id): """Remove a favorite row from the favorites table based on user_id and pet_id.""" with closing(connect_db()) as conn: cursor = conn.cursor() - - # Execute DELETE query to remove the row matching user_id and pet_id + + # Execute DELETE query to remove the row matching user_id and pet_id cursor.execute(""" DELETE FROM favorites WHERE user_id = ? AND pet_id = ? """, (user_id, pet_id)) - + conn.commit() return cursor.rowcount # Returns the number of rows affected (should be 1 if successful) -# ----------------------------------------------------------------------------------------------------------- change fucntions +# ------------------------------------------- change fucntions def change_user_attributes(user_id, attribute, change): """Update a user's attribute in the accounts table.""" - + # List of valid column names to avoid SQL injection - valid_columns = ['name', 'age', 'location'] # Add your valid column names here - + valid_columns = ['name', 'age', 'location', 'password'] # Add your valid column names here + if attribute not in valid_columns: raise ValueError("Invalid attribute name.") - + with closing(connect_db()) as conn: cursor = conn.cursor() - + # Use the attribute directly, now it's safe since we validated it cursor.execute(f""" UPDATE accounts SET {attribute} = ? WHERE user_id = ? """, (change, user_id)) - + conn.commit() return fetch_user_by_id(user_id) def change_pet_attributes(pet_id, attribute, change): """Update a user's attribute in the accounts table.""" - + # List of valid column names to avoid SQL injection - valid_columns = ['name', 'age', 'location', 'breed', 'gender', 'type', 'photo_path'] # Add your valid column names here - + valid_columns = ['name', 'age', 'location', 'breed', + 'gender', 'type', 'photo_path'] # Add your valid column names here + if attribute not in valid_columns: raise ValueError("Invalid attribute name.") - + with closing(connect_db()) as conn: cursor = conn.cursor() - + # Use the attribute directly, now it's safe since we validated it cursor.execute(f""" UPDATE pets SET {attribute} = ? WHERE pet_id = ? """, (change, pet_id)) - + conn.commit() return fetch_pet_by_id(pet_id) -# ----------------------------------------------------------------------------------------------------------- fetch functions +# ------------------------------------- fetch functions + + +def fetch_userid_from_username_and_password(username,password): + """Get a users ID from there username and password""" + + with closing(connect_db()) as conn: + cursor = conn.cursor() + + # Execute DELETE query to remove the row matching user_id and pet_id + cursor.execute(""" + SELECT user_id FROM accounts + WHERE username = ? AND password = ? + """, (username, password)) + + result = cursor.fetchone() + if result: + return result[0] + else: + return None def fetch_favorites_by_user(user_id): """Fetch all favorite pets for a given user_id.""" with closing(connect_db()) as conn: cursor = conn.cursor() - + cursor.execute(""" SELECT pets.* FROM pets INNER JOIN favorites ON pets.pet_id = favorites.pet_id WHERE favorites.user_id = ? """, (user_id,)) - + return cursor.fetchall() diff --git a/Backend/databaseinit.py b/Backend/database_files/initdatabase.py similarity index 52% rename from Backend/databaseinit.py rename to Backend/database_files/initdatabase.py index 861c9a8..e90b8d4 100644 --- a/Backend/databaseinit.py +++ b/Backend/database_files/initdatabase.py @@ -1,20 +1,20 @@ -'''ONLY USE THIS IF THE DATABASE IS EMPTY. IT'S THE INIT CODE''' +'''INIT TABLES IN DATABASE''' import sqlite3 -from contextlib import closing -import os -def populate_test_data(db_path): +def create_database_tabels(db_path): '''THE ACTUAL FUNCITON''' conn = sqlite3.connect(db_path) cursor = conn.cursor() - conn.execute("PRAGMA foreign_keys = ON") + conn.execute("PRAGMA foreign_keys = ON") # Create tables cursor.executescript(''' CREATE TABLE IF NOT EXISTS accounts ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + password TEXT NOT NULL, name TEXT NOT NULL, age INTEGER, location TEXT, @@ -41,35 +41,21 @@ def populate_test_data(db_path): created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); - ''') - # Insert sample users - cursor.executescript(''' - INSERT INTO accounts (name, age) VALUES - ('Ben Ten', 28), - ('Spongebob Smith', 35), - ('Dipper Pines', 25), - ('Emilia Kirej', 20); - ''') + INSERT INTO pets (name, age, gender, breed, type, location, photo_path) + VALUES + ('Bella', 3, 'female', 'Golden Retriever', 'dog', 'New York', 'images/Chuck.jpg'), + ('Max', 5, 'male', 'Maine Coon', 'cat', 'Boston', 'images/gunner.jpg'), + ('Luna', 2, 'female', 'Siamese', 'cat', 'Chicago', 'images/luna.jpg'), + ('Charlie', 4, 'male', 'Beagle', 'dog', 'Los Angeles', 'images/lizzie_izzie.jpg'), + ('Daisy', 1, 'female', 'Labrador Retriever', 'dog', 'Miami', 'images/monster.jpg'); - # Insert sample pets - cursor.executescript(''' - INSERT INTO pets (name, age, gender, breed, type, location, photo_path) VALUES - ('Gunner', 9, 'male', 'Golden Retriever', 'dog', 'Myrtle Beach, SC', 'images/gunner.jpg'), - ('Lizzie Izzie', 5, 'female', 'Donskoy', 'cat', 'Milford, CT', 'images/lizzie_izzie.jpg'), - ('Chuck', 1, 'male', 'Corgi', 'dog', 'Orlando, FL', 'images/chuck.jpg'), - ('Monster', 5, 'female', 'Maine Coon', 'cat', 'Houston, TX', 'images/monster.jpg'); - ''') - # Insert sample favorites - cursor.executescript(''' - INSERT INTO favorites (user_id, pet_id) VALUES - (2, 4), - (1, 2); ''') + conn.commit() conn.close() if __name__ == "__main__": - populate_test_data('Projectdatabase.db') + create_database_tabels('projectdatabase.db') diff --git a/Backend/docker_files/Dockerfile b/Backend/docker_files/Dockerfile new file mode 100644 index 0000000..305c2f8 --- /dev/null +++ b/Backend/docker_files/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.9-slim +WORKDIR /Docker1 + + +COPY /database_files/databasefunctions.py /Docker1/database_files/databasefunctions.py +COPY app.py app.py +COPY /database_files/initdatabase.py /Docker1/database_files/initdatabase.py + + +# Copy requirements.txt from docker_files directory into the container +COPY docker_files/requirements.txt /Docker1/requirements.txt + +# Install Python dependencies inside Docker +RUN pip3 install -r /Docker1/requirements.txt + +EXPOSE 5000 + +CMD python3 /Docker1/database_files/initdatabase.py && python3 app.py diff --git a/Backend/docker_files/requirements.txt b/Backend/docker_files/requirements.txt new file mode 100644 index 0000000..2e64847 --- /dev/null +++ b/Backend/docker_files/requirements.txt @@ -0,0 +1,4 @@ +Flask==3.0.3 +requests==2.26.0 +pytest==7.2.1 +flask_cors==3.0.10 diff --git a/Backend/requirements.txt b/Backend/requirements.txt deleted file mode 100644 index 5eeda4f..0000000 --- a/Backend/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -Flask==2.0.3 -requests==2.26.0 -pandas==1.3.4 -pytest==7.2.1 -flask_cors==3.0.10 -#Werkzeug==2.1.3 -#pip freeze - diff --git a/Backend/testData.db b/Backend/testData.db deleted file mode 100644 index 14a1ced..0000000 Binary files a/Backend/testData.db and /dev/null differ diff --git a/Backend/test_app.py b/Backend/test_app.py new file mode 100644 index 0000000..da5e088 --- /dev/null +++ b/Backend/test_app.py @@ -0,0 +1,133 @@ +""" +Pytest for fake API +""" +import requests + +UNIQUE_USER_ID = None +UNIQUE_PET_ID = None + + +def test_insert_user(): + """ + Test for http://localhost:5000/add_newuser, adding user to database + """ + global UNIQUE_USER_ID + url = "http://localhost:5000" + + UNIQUE_USER_ID = requests.post((url + "/add_newuser"), + json={"username":"sam2213", + "password":"catlover20","name": "sam", + "age": 17, "location": "jamaica"},timeout=10) + + response1 = requests.post((url + "/fetch_user"),json=UNIQUE_USER_ID.json(), timeout=10) + response2 = requests.post((url + "/fetch_allusers"), timeout=10) + + assert (response1.json() in response2.json()) is True + + +def test_insert_pet(): + """ + Test for http://localhost:5000/add_newpet, adding new pet to database to database + """ + global UNIQUE_PET_ID + url = "http://localhost:5000" + UNIQUE_PET_ID = requests.post((url + "/add_newpet"), + json={"name": "sam", "age": 17, + "gender":"female","breed":"pug", + "type":"cat","location": "jamaica", + "photo_path":"images/sam.jpg"},timeout=10) + + response1 = requests.post((url + "/fetch_pet"),json=UNIQUE_PET_ID.json(), timeout=10) + response2 = requests.post((url + "/fetch_allpets"), timeout=10) + + assert (response1.json() in response2.json()) is True + +def test_insert_favorite_relation(): + """ + Test for http://localhost:5000/add_newfavorite, adding users favorited pet into the database + """ + global UNIQUE_USER_ID + global UNIQUE_PET_ID + + url = "http://localhost:5000" + + + requests.post((url + "/add_newfavorite"), + json={"user_id": UNIQUE_USER_ID.json().get("user_id"), + "pet_id": UNIQUE_PET_ID.json().get("pet_id")},timeout=10) + + response1 = requests.post((url + "/fetch_pet"),json=UNIQUE_PET_ID.json(), timeout=10) + response2 = requests.post((url + "/fetch_favorited_pets"),json=UNIQUE_USER_ID.json(),timeout=10) + + assert (response1.json() in response2.json()) is True + + +# ----------------------------------------------------------------------------- + +def test_fetch_userid(): + """ + Test for http://localhost:5000/fetch_userid, + retrieve userid accosiated with username and password + """ + + global UNIQUE_USER_ID + global UNIQUE_PET_ID + + url = "http://localhost:5000" + userid = requests.post((url + "/fetch_userid"), + json={"username": "sam2213", + "password": "catlover20"}, timeout=10) + + assert userid.json() == UNIQUE_USER_ID.json().get("user_id") + +# ---------------------------------------------------------------------------------- + +def test_remove_favorite_relation(): + """ + Test for http://localhost:5000/remove_favorite, remove users favorited pet from the database + """ + + global UNIQUE_USER_ID + global UNIQUE_PET_ID + + url = "http://localhost:5000" + + requests.delete((url + "/remove_favorite"), + json={"user_id": UNIQUE_USER_ID.json().get("user_id"), + "pet_id": UNIQUE_PET_ID.json().get("pet_id")},timeout=10) + + + response = requests.post((url + "/fetch_favorited_pets"),json=UNIQUE_USER_ID.json(),timeout=10) + + assert (response.json()) == [] + + +def test_remove_user(): + """ + Test for http://localhost:5000/remove_user + """ + + global UNIQUE_USER_ID + global UNIQUE_PET_ID + + url = "http://localhost:5000" + + requests.delete((url + "/remove_user"), json=UNIQUE_USER_ID.json(), timeout=10) + + response = requests.post((url + "/fetch_user"),json=UNIQUE_USER_ID.json(), timeout=10) + + assert response.json() is None + + +def test_remove_pet(): + """ + Test for http://localhost:5000/remove_pet + """ + + url = "http://localhost:5000" + + requests.delete((url + "/remove_pet"), json=UNIQUE_PET_ID.json(), timeout=10) + + response = requests.post((url + "/fetch_pet"),json=UNIQUE_PET_ID.json(), timeout=10) + + assert response.json() is None diff --git a/README.md b/README.md index 7a7c605..112d09b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ -# cse2102-fall24-Team51 -Eric Asante eoa21004 -Kyle Kirejczyk kek20009 -Ishayu Ray isr21011 -Kunal bagga kub21001 -Link to Trello: https://trello.com/b/j8t0Ulvk/group-51-jira-kaban-board -Link to Figma Prototype: https://www.figma.com/design/mKMWW1sIOVpuQKOlapBINQ/ishayu.ray \ No newline at end of file +# CSE2102-fall24-Team51 + +Eric Asante eoa21004
+Kyle Kirejczyk kek20009
+Ishayu Ray isr21011
+Kunal Bagga kub21001
+ +Link to Trello: [https://trello.com/b/j8t0Ulvk/group-51-jira-kaban-board](https://trello.com/b/j8t0Ulvk/group-51-jira-kaban-board)
+Link to Figma Prototype: [https://www.figma.com/design/mKMWW1sIOVpuQKOlapBINQ/ishayu.ray](https://www.figma.com/design/mKMWW1sIOVpuQKOlapBINQ/ishayu.ray)
+ +# Terminal commands to run DOCKERIZED API:
+Make sure you cd into Backend folder.
+1. docker build -f docker_files/Dockerfile -t team51-backend .
+2. docker run -d -p 5000:5000 team51-backend
+3. pytest