diff --git a/app.py b/app.py index c908969..7c10f3c 100644 --- a/app.py +++ b/app.py @@ -1,18 +1,110 @@ -import re -from flask import Flask, url_for, render_template +from flask import Flask, redirect, url_for, render_template, request, session +# from flask_pymongo import PyMongo import model +import bcrypt + app = Flask(__name__) -# app.config.from_object('config') +app.config.from_object('config') +mongo = model.PyMongoFixed(app) +db = mongo.db +if "users" not in db.list_collection_names(): + db.create_collection("users") + #TODO: Use save_file to store default.jpg for non-logged in users or default pic. + + +@app.route('/file/', methods=["GET"]) +def file(filename): + """Helper route for getting files from database. Functions as wrapper for + mongo.send_file function. + + Args: + filename (str): file to return + """ + return mongo.send_file(filename) + +@app.route('/login',methods=['GET','POST']) +def login(): + """Login form for users. Sends POST request to itself. If it + validates the user, redirects to index page. Taken mostly from class + slides. + """ + if request.method == "POST": + users = db.users + login_user = users.find_one({'username':request.form['username']}) + if login_user: + db_password = login_user['password'] + password = request.form['password'].encode('utf-8') + if bcrypt.checkpw(password,db_password): + session['username'] = request.form['username'] + return redirect(url_for('index')) + else: + return 'Invalid username/password combination.' + else: + return 'User not found.' + else: + return render_template('login.html') + +@app.route('/logout') +def logout(): + """Logging out endpoint. Clears all local + session information and redirects to index. + """ + session.clear() + return redirect('/') + +@app.route('/create_user', methods=['POST']) +def create_user(): + """Helper route for creating users.Takes in form information and + pushes user to database. Should only be accessed from signup route. + + Args: + Form: + username (str) + password (str) + email (str) + profile_pic (file) + """ + users = db.users + existing_user = users.find_one({'name':request.form['username']}) + if not existing_user: + if 'profile_image' in request.files: + pf = request.files['profile_image'] + filename = model.hash_profile_name(pf.filename) + user = model.User( + request.form['username'], + pswd=request.form['password'], + email=request.form['email'].strip(), + profile_pic=filename + ) + mongo.save_file(filename,pf) + else: + user = model.User( + request.form['username'], + pswd=request.form['password'], + email=request.form['email'].strip() + ) + users.insert_one(user.to_json()) + session['username'] = user.username + return redirect(url_for('index')) + +@app.route('/signup', methods=["GET"]) +def signup(): + """Endpoint for 'signup.html' + """ + return render_template('signup.html') @app.route("/") @app.route("/index") def index(): + """Route for index page. Renders 'index.html' file. Currently has + placeholder data for debugging/visualization of work. + """ company1 = model.Company( name="Microsoft", - category="software", + category="Software", logo_img="https://bit.ly/3uWfYzK", banner_img="https://bit.ly/3xfolJs" ) @@ -22,22 +114,24 @@ def index(): logo_img="https://bit.ly/3Jvmy5t", banner_img="http://somelink.com" ) - review_1 = model.Review( - company1, - "Placeholder Review", - "Security Engineering", - "Security Engineer Intern", - 4, - "B.S.", - "Had a good time overall. Tasking was tough and hours were long.", - 5, - offer=True - ) - review_2 = model.Review(company2, "Title", 'Software Engineering', - "Position", company_rating=4, education="M.S.", interview_desc="Interview", - interview_rat=4, offer=False, accepted=False, start_date="05-23-2022", - intern_desc="desc", work_rat=None, culture_rat=None, location=("San Francisco", "California"), - pay=35.25, bonuses="Bonus") + review_1 = model.Review('user',company1,"Placeholder Review","Security Engineering", + "Security Engineer Intern",company_rating=4,education="B.S.", + interview_desc="Had a good time overall. Tasking was tough and hours were long.", + interview_rat=5,offer=False, accepted=False, start_date="05-23-2022", + intern_desc="desc", work_rat=None, culture_rat=None, location=("San Francisco", "California"), + pay=35.25, bonuses="Bonus") + review_2 = model.Review('user',company2, "Title", 'Software Engineering', + "Position", company_rating=4, education="M.S.", interview_desc="Interview", + interview_rat=4, offer=False, accepted=False, start_date="05-23-2022", + intern_desc="desc", work_rat=None, culture_rat=None, location=("San Francisco", "California"), + pay=35.25, bonuses="Bonus") placeholder = [review_1 for _ in range(3)] placeholder.extend([review_2 for _ in range(3)]) - return render_template("index.html", recent_reviews=placeholder) \ No newline at end of file + + if 'username' in session: + current_user = db.users.find_one({"username":session['username']}) + if not current_user: + return render_template("index.html", recent_reviews=placeholder, user=None) + return render_template("index.html", recent_reviews=placeholder, user=current_user) + return render_template("index.html", recent_reviews=placeholder, user=None) + diff --git a/config.py b/config.py index e69de29..6558dbf 100644 --- a/config.py +++ b/config.py @@ -0,0 +1,4 @@ +import os +SECRET_KEY = os.urandom(16).hex() +#TODO: reset this password bc it's in plaintext +MONGO_URI= f"mongodb+srv://admin:Rqs7wmYA1qkMzh8X@pathway.rgqhh.mongodb.net/pathway?retryWrites=true&w=majority" \ No newline at end of file diff --git a/model.py b/model.py index 6b7eb0f..8c535ec 100644 --- a/model.py +++ b/model.py @@ -1,7 +1,11 @@ from datetime import datetime +import bcrypt +import re + +from flask_pymongo import PyMongo company_categories = ["Software", "Hardware", "Computing", "Finance", "Government", "Defense", "Aerospace", - "Restaurant", "Automobiles", "Aviation", "Retail", "Other"] + "Restaurant", "Automobiles", "Aviation", "Retail", "Other"] job_categories = ["Software Engineering", "Computer Science", "Information Technology", "System Administrator", "Computer Engineering", "Eletrical Engineering", "Data Science", "Security Engineering", @@ -71,14 +75,26 @@ def is_url(url:str)->bool: Args: url (str): The url to validate + Returns: - bool indicating validity - """ - from urllib.parse import urlparse - try: - result = urlparse(url) - return all([result.scheme, result.netloc]) - except: + bool: Indicates URL validity + # """ + regex = ("((http|https)://)(www.)?" + "[a-zA-Z0-9@:%._\\+~#?&//=]" + "{2,256}\\.[a-z]" + + "{2,6}\\b([-a-zA-Z0-9@:%" + "._\\+~#?&//=]*)") + + # Compile the ReGex + p = re.compile(regex) + + # If the string is empty + # return false + if (url is None): + return False + + # Return if the string + # matched the ReGex + if(re.search(p, url)): + return True + else: return False def validate_date(date:str)->None: @@ -100,8 +116,71 @@ def validate_date(date:str)->None: except ValueError: raise ValueError("Incorrect date format. Should be MM-DD-YYYY") +def validate_email(email:str)->bool: + """Validates an email. + + Args: + email (str): The email string to validate. + + Returns: + bool: A boolean indicating whether the email, passed in as a parameter, is valid. + """ + regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + if re.fullmatch(regex,email): + return True + return False + +def hash_profile_name(name:str): + """Helper method for hashing profile names. Used to avoid collisions in filenames + when uploading profile pictures to the database. Method should generate entirely + unique hashes stored both in the user object and separately by the database. + + Args: + name (str): name to hash + + Returns: + str: String of hashed name encoded in utf-8. + """ + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(name.encode("utf-8"),salt) + return hashed.decode("utf-8") + + class Company: + """Represents an existing and reviewable company. + Contains internal information on the amount + of reviews submitted to that company as well + as the company's status on Pathway. + + Attributes: + name: A string, representing the company's name. + category: A string, representing the category the company belongs to. + logo_img: + description: A string, representing the company's description. + banner_img: + company_rat: The average of the company's overall rating, as a float. + work_rat: The average of the company's workplace reviews, as a float. + culture_rat: A float denoting the average of the company's culture reviews. + num_company_reviews: The total number of ratings under the overall rating category, as an integer. + num_work_reviews: The total number of workplace reviews, as an integer. + num_culture_reviews: An integer denoting the total number of work culture reviews. + total_reviews: An integer representing the overall, singular number of reviews. + status: A string representing the company's status. Must be a valid CSS color keyword. + """ def __init__(self, name:str, category:str, logo_img:str, description:str="", banner_img:str="")->None: + """Initializes a reviewable Company. + + Args: + name (str): The company's name. + category (str): The category the company falls under. + logo_img (str): + description (str, optional): A description on the current company. + banner_img (str, optional): + + Raises: + TypeError: Raised if any of the arguments aren't of the expected types. + ValueError: Raised if the company's name, categories, and more are of the correct types but not supported. + """ #type checks if type(name) is not str: raise TypeError(f"Error: Company name must a string. Given type '{type(name)}'.") @@ -121,6 +200,9 @@ def __init__(self, name:str, category:str, logo_img:str, description:str="", ban if len(category) == 0: raise ValueError("Error: Company category cannot be empty.") + if category not in company_categories: + raise ValueError("Error: Invalid company category.") + if not is_url(logo_img): raise ValueError("Error: Invalid URL given for company logo.") @@ -150,10 +232,127 @@ def __init__(self, name:str, category:str, logo_img:str, description:str="", ban # the previous entries dont count # as individual reviews self.total_reviews = 0 + # a company will have a color border + # around them depending on their rating. + # the status must be a supported CSS + # color keyword + self.status = "grey" + + def update_status(self)->None: + """Updates the company's status. A company + status is a color indicating whether the + company has had favorable reviews or not. + This manifests around the company logo's + circumference on the front end. Changes + based on what numerical ranges the company's + overall reviews fall under. + """ + if self.company_rat is not None: + if self.company_rat == 5: + self.status = "green" + + elif self.company_rat >= 4 and self.company_rat < 5: + self.status = "lightgreen" + + elif self.company_rat >= 3 and self.company_rat < 4: + self.status = "orange" + + elif self.company_rat >= 2 and self.company_rat < 3: + self.status = "red" + + # rating of 0-1.99 + else: + self.status = "black" class User: - def __init__(self, username:str, email:str, pswd:str, profile_pic:str="")->None: - pass + """Represents a user class. Contains the user's information such as + their username, email, password, and more. + + Attributes: + username: A string, representing the user's username. + email: The user's email, as a string. + password: The user's confirmed password, as plaintext. Hashed once the account is created. + profile_pic: + creation_time: A datetime object, containing the time the user account was created. + """ + def __init__(self, username:str, email:str, pswd:str, profile_pic:str="default.jpg")->None: + """Initializes a user given the information + the user decides to input. + + Args: + username (str): The user's username. + email (str): The user's email. + pswd (str): The user's password, as plaintext. + profile_pic (str, optional): + + Raises: + TypeError: Raised if none of the parameters match the expected types. + ValueError: Raised if the parameters match the expected types but fail any validation checks. + """ + #type checks + if type(username) is not str: + raise TypeError(f"Error: username must a string. Given type '{type(username)}'.") + if type(email) is not str: + raise TypeError(f"Error: email must a string. Given type '{type(email)}'.") + if type(pswd) is not str: + raise TypeError(f"Error: password must a string. Given type '{type(pswd)}'.") + if type(profile_pic) is not str: + raise TypeError(f"Error: profile_pic must a string. Given type '{type(profile_pic)}'.") + + #value checks + if len(username) <= 3: + raise ValueError("Error: usernames must have more than 3 characters.") + if not validate_email(email): + raise ValueError(f"Error: Invalid email. Given email {email} with type {type(email)} ") + if len(pswd) < 8: + raise ValueError("Error: Password must be 6 or more characters.") + if len(profile_pic) == 0: + raise ValueError("Error: Invalid Profile Picture name.") + + self.username = username + self.email = email + self.password = self.generate_password(pswd) + self.profile_pic = profile_pic + self.creation_time = datetime.now() + + def generate_password(self, unhashed:str)->bytes: + """Generates a secure, hashed version of + the user's password, initially passed + in as plaintext, with bcrypt's methods. + + Args: + unhashed (str): The user's password as plaintext. + + Returns: + bytes: A hashed representation of the user's password, in bytes. + """ + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(unhashed.encode("utf-8"), salt) + return hashed + + def set_profile_pic(self,link:str)->bool: + if type(link) is not str: + raise TypeError("Error: Link to profile picture must be a string.") + + if not is_url(link): + return False + self.profile_pic = link + return True + + def to_json(self)->dict: + """Generates a copy of the User object as a dictionary for use as JSON. + + Returns: + dict: Representing the user object attributes. + + """ + return { + "username":self.username, + "email":self.email, + "password":self.password, + "profile_pic":self.profile_pic, + "creation_time":self.creation_time + } class Review: """Represents a review posted to a specific company. @@ -163,6 +362,7 @@ class Review: based on what the user rated. Attributes: + user: A string indicating which user account created the review. company: The Company object of the company to be rated by the user. job_cat: The general job category the internship falls under, as a string. position: A string representing the position the user had at the company. @@ -181,7 +381,7 @@ class Review: bonuses: A string of any extra bonuses the company might offer at the internship. date_posted: A datetime object, representing the date the internship was posted. """ - def __init__(self, company:'Company', title:str, job_cat:str, position:str, company_rating:int, education:str, + def __init__(self, user:str, company:'Company', title:str, job_cat:str, position:str, company_rating:int, education:str, interview_desc:str, interview_rat:int, offer:bool=False, accepted:bool=False, start_date:str="", intern_desc:str="", work_rat:int=None, culture_rat:int=None, location:tuple=("None", "Remote"), pay:float=None, bonuses:str="")->None: @@ -190,6 +390,7 @@ def __init__(self, company:'Company', title:str, job_cat:str, position:str, comp the fields the user decides to fill out. Args: + user(str): The username of the account that created the review. company (Company): The company to rate. job_cat (str): The general job category the internship falls under. position (str): The position held at the company. @@ -213,6 +414,11 @@ def __init__(self, company:'Company', title:str, job_cat:str, position:str, comp ValueError: Raised if the arguments are of the correct types but aren't supported by the website. """ + if type(user) != str: + raise TypeError("Username that created the review must be a string!") + + if not user or len(user) <= 3: + raise ValueError("Invalid username!") if type(company) != Company: raise TypeError("Company to review must be a company object!") @@ -329,6 +535,7 @@ def __init__(self, company:'Company', title:str, job_cat:str, position:str, comp if type(bonuses) != str: raise TypeError("Any additional info on bonuses must be a string!") + self.user = user self.company = company self.job_cat = job_cat self.position = position @@ -419,4 +626,73 @@ def score_formula(old_score:float, num_reviews:int, new_score:int)->float: self.company.num_culture_reviews += 1 -local_companies = {Company("Microsoft", "Software", "https://bit.ly/3uWfYzK", banner_img="https://bit.ly/3xfolJs")} \ No newline at end of file +local_companies = {Company("Microsoft", "Software", "https://bit.ly/3uWfYzK", banner_img="https://bit.ly/3xfolJs")} + + + +class PyMongoFixed(PyMongo): + """A small magic trick Class that functions as a Wrapper for PyMongo. + Overwrites a broken flask_pymongo 2.3.0 function to fetch image data from + the database. See https://github.com/dcrosta/flask-pymongo/issues/153 + to learn more. + """ + + def __init__(self, app=None, uri=None, *args, **kwargs): + super().__init__(app, uri, *args, **kwargs) + self.text_type = str + self.num_type = int + + + def send_file(self, filename, base="fs", version=-1, cache_for=31536000): + """Respond with a file from GridFS. + + Returns an instance of the :attr:`~flask.Flask.response_class` + containing the named file, and implement conditional GET semantics + (using :meth:`~werkzeug.wrappers.ETagResponseMixin.make_conditional`). + + .. code-block:: python + + @app.route("/uploads/") + def get_upload(filename): + return mongo.send_file(filename) + + :param str filename: the filename of the file to return + :param str base: the base name of the GridFS collections to use + :param bool version: if positive, return the Nth revision of the file + identified by filename; if negative, return the Nth most recent + revision. If no such version exists, return with HTTP status 404. + :param int cache_for: number of seconds that browsers should be + instructed to cache responses + """ + from flask import abort, current_app, request + from gridfs import GridFS, NoFile + from werkzeug.wsgi import wrap_file + + if not isinstance(base, self.text_type): + raise TypeError("'base' must be string or unicode") + if not isinstance(version, self.num_type): + raise TypeError("'version' must be an integer") + if not isinstance(cache_for, self.num_type): + raise TypeError("'cache_for' must be an integer") + + storage = GridFS(self.db, base) + + try: + fileobj = storage.get_version(filename=filename, version=version) + except NoFile: + abort(404) + + # mostly copied from flask/helpers.py, with + # modifications for GridFS + data = wrap_file(request.environ, fileobj, buffer_size=1024 * 255) + response = current_app.response_class( + data, + mimetype=fileobj.content_type, + direct_passthrough=True, + ) + response.content_length = fileobj.length + response.last_modified = fileobj.upload_date + response.cache_control.max_age = cache_for + response.cache_control.public = True + response.make_conditional(request) + return response \ No newline at end of file diff --git a/static/Images/default.jpg b/static/Images/default.jpg new file mode 100644 index 0000000..7b940c8 Binary files /dev/null and b/static/Images/default.jpg differ diff --git a/templates/index.html b/templates/index.html index 688b913..244576e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,7 +15,14 @@

Read Reviews on top companies from fellow students.

+ + {% if user %} + sample + {% else %} + sample + {% endif %} +
    {% for review in recent_reviews %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..93c71d2 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% block htmlclass %}class="login{% endblock %} + +{% block head %} +Pathway - Login +{% endblock %} + +{% block bodyclass %}class="login"{% endblock %} + +{% block main %} +
    + + + +
    +{% endblock %} \ No newline at end of file diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..f1c712e --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block htmlclass %}class="signup"{% endblock %} + +{% block head %} +Pathway - Sign up +{% endblock %} + +{% block bodyclass %}class="signup"{% endblock %} + +{% block main %} +
    + + + + + +
    +{% endblock %} \ No newline at end of file diff --git a/tests/test_company.py b/tests/test_company.py index c5e8b10..b81ef24 100644 --- a/tests/test_company.py +++ b/tests/test_company.py @@ -4,34 +4,81 @@ class TestCompany(unittest.TestCase): def setUp(self): self.valid_link = "http://somelink.com" #used for ease of writing tests + self.valid_category = "Software" self.company1 = Company( name="Microsoft", - category="software", + category=self.valid_category, logo_img="https://bit.ly/3uWfYzK", banner_img="https://bit.ly/3xfolJs" ) self.company2 = Company( name="Google", - category="Software", + category=self.valid_category, logo_img="http://somelink.com", banner_img="http://somelink.com" ) def test_types(self): - self.assertRaises(TypeError, Company, None, "software", self.valid_link,self.valid_link) + self.assertRaises(TypeError, Company, None, self.valid_category, self.valid_link,self.valid_link) self.assertRaises(TypeError, Company, "Netflix", 2, self.valid_link, self.valid_link) - self.assertRaises(TypeError, Company, "Home Depot", "software", [], self.valid_link) - self.assertRaises(TypeError, Company, "Netflix", "software", self.valid_link, None) - self.assertRaises(TypeError, Company, "Netflix", "software", self.valid_link, self.valid_link, + self.assertRaises(TypeError, Company, "Home Depot", self.valid_category, [], self.valid_link) + self.assertRaises(TypeError, Company, "Netflix", self.valid_category, self.valid_link, None) + self.assertRaises(TypeError, Company, "Netflix", self.valid_category, self.valid_link, self.valid_link, description=333) def test_values(self): - self.assertRaises(ValueError, Company, "", "software", self.valid_link, self.valid_link) + #name test + #it's kind of hard to determine what's a valid company name + self.assertRaises(ValueError, Company, "", self.valid_category, self.valid_link, self.valid_link) + #category test self.assertRaises(ValueError, Company, "Netfix", "", self.valid_link, self.valid_link) - self.assertRaises(ValueError, Company, "Netflix", "software", "htp/broken_link", self.valid_link) - self.assertRaises(ValueError, Company, "Netflix", "software", self.valid_link, "desc", "link") + self.assertRaises(ValueError, Company, "Netfix", "invalid", self.valid_link, self.valid_link) + self.assertRaises(ValueError, Company, "Netfix", "softwre", self.valid_link, self.valid_link) + self.assertRaises(ValueError, Company, "Netfix", "111", self.valid_link, self.valid_link) + #logo_img test + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, "htp/broken_link", self.valid_link) + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, "http://", self.valid_link) + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, "htp/www.broken.com", self.valid_link) + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, "broken.com", self.valid_link) + #banner_img test + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, self.valid_link, banner_img="brkn") + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, self.valid_link, banner_img="http://www.") + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, self.valid_link, banner_img="http://") + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, self.valid_link, banner_img="broken.com") + self.assertRaises(ValueError, Company, "Netflix", self.valid_category, self.valid_link, banner_img="http://invalid") + def test_equality(self): + # default status color + self.assertEqual(self.company1.status, "grey") + # perfect score + self.company1.company_rat = 5 + self.company1.update_status() + self.assertEqual(self.company1.status, "green") + # should be lightgreen + self.company1.company_rat = 4.99 + self.company1.update_status() + self.assertEqual(self.company1.status, "lightgreen") + # just at the edge of 4 + self.company1.company_rat = 3.9 + self.company1.update_status() + self.assertEqual(self.company1.status, "orange") + # at the edge of 3 + self.company1.company_rat = 2.99 + self.company1.update_status() + self.assertEqual(self.company1.status, "red") + # exact value + self.company1.company_rat = 2 + self.company1.update_status() + self.assertEqual(self.company1.status, "red") + # any score less than 2 + self.company1.company_rat = 1.99 + self.company1.update_status() + self.assertEqual(self.company1.status, "black") + # score of 0 + self.company1.company_rat = 0 + self.company1.update_status() + self.assertEqual(self.company1.status, "black") if __name__ == "__main__": unittest.main(failFast=True) \ No newline at end of file diff --git a/tests/test_reviews.py b/tests/test_reviews.py index 8d52bb1..e521466 100644 --- a/tests/test_reviews.py +++ b/tests/test_reviews.py @@ -1,12 +1,13 @@ -from model import Company, Review +from model import Company, Review, User import unittest class TestReviews(unittest.TestCase): def setUp(self): self.comp1 = Company("Amazon", "Software", "http://somelink.com") self.comp2 = Company("McDonald's", "Restaurant", "http://somelink.com") + self.user = User("Test", "somemail@gmail.com", "123323456") - self.review1 = Review(self.comp1, "Title", 'Software Engineering', + self.review1 = Review('User', self.comp1, "Title", 'Software Engineering', "Position", company_rating=4, education="M.S.", interview_desc="Interview", interview_rat=4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=None, culture_rat=None, location=("San Francisco", "California"), @@ -15,204 +16,251 @@ def setUp(self): def test_init_required(self): 'Type Errors' - self.assertRaises(TypeError, Review, 'Boeing', "Title", 'Software Engineering', + #invalid user + self.assertRaises(TypeError, Review, self.user, self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4) - self.assertRaises(TypeError, Review, self.comp1, {"Title"}, 'Software Engineering', + self.assertRaises(TypeError, Review, ["User"], self.comp1, "Title", 'Software Engineering', + "Position", 4, "M.S.", "Interview", 4) + + # not a company object + self.assertRaises(TypeError, Review, 'User', 'Boeing', "Title", 'Software Engineering', + "Position", 4, "M.S.", "Interview", 4) + + # title is of the wrong type + self.assertRaises(TypeError, Review, 'User', self.comp1, {"Title"}, 'Software Engineering', "Position", 4, "M.S.", "Interview", 4) - self.assertRaises(TypeError, Review, self.comp1, "Title", + # Position is None + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', None, 4, "M.S.", "Interview", 4) - self.assertRaises(TypeError, Review, self.comp1, "Title", ['Software Engineering'], + # job is of the wrong type + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", ['Software Engineering'], "Position", 4, "M.S.", "Interview", 4) - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # position is of the wrong type + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', ["Position"], 4, "M.S.", "Interview", 4) - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # score is of the wrong type + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", hex(4), "M.S.", "Interview", 4) - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # score cant be a float + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 3.90, "M.S.", "Interview", 4) - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 0.30, "M.S.", "Interview", 4) - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + # education is of the wrong type + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, ord("M"), "Interview", 4) - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + # description is of the wrong time + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", ["Review"], 4) - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + # score is of the wrong type + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Review", hex(4)) - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Review", 4.90) - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Review", 3.50) - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Review", float(2)) 'Value Errors' - self.assertRaises(ValueError, Review, self.comp1, "", 'Software Engineering', + # user is empty or not the right length + self.assertRaises(ValueError, Review, "", self.comp1, "Title", 'Software Engineering', + "Position", 4, "M.S.", "Interview", 4) + + self.assertRaises(ValueError, Review, "chr", self.comp1, "Title", 'Software Engineering', + "Position", 4, "M.S.", "Interview", 4) + + # title is empty + self.assertRaises(ValueError, Review, 'User', self.comp1, "", 'Software Engineering', "Position", 4, "M.S.", "Review", 4) - self.assertRaises(ValueError, Review, self.comp1, "Title", 'Software Engineering', + # position is empty + self.assertRaises(ValueError, Review, 'User', self.comp1, "Title", 'Software Engineering', "", 4, "M.S.", "Review", 4) + # SWE is not a job category + self.assertRaises(ValueError, Review, 'User', self.comp1, "Title", 'SWE', "Position", 4, "M.S.", "Review", 4) - self.assertRaises(ValueError, Review, self.comp1, "Title", 'SWE', "Position", 4, "M.S.", "Review", 4) - - self.assertRaises(ValueError, Review, self.comp1, "Title", '', "Position", 4, "M.S.", "Review", 4) + # category is empty + self.assertRaises(ValueError, Review, 'User', self.comp1, "Title", '', "Position", 4, "M.S.", "Review", 4) - self.assertRaises(ValueError, Review, self.comp1, "Title", 'Software Engineering', + # rating more or less than the max or min allowed + self.assertRaises(ValueError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 10, "M.S.", "Review", 4) - self.assertRaises(ValueError, Review, self.comp1, "Title", 'Software Engineering', + self.assertRaises(ValueError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", -1, "M.S.", "Review", 4) - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # invalid education + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "", "Review", 4) - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "A degree", "Review", 4) - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # invalid description + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "", 4) - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # rating is invalid + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Review", -3) - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Review", 6) def test_init_optional(self): 'Type Errors' - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # offer not a bool + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=0, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # accepted not a bool + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=None, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # start date of the wrong type + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date=["05", "23", "2022"], intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # intern desc is None + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc=None, work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # work rating of invalid type + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat="10", culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # culture rating of invalid type + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=hex(20), location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # location not a tuple + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=["San Francisco", "California"], pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # pay is a string + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay="23.64", bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp1, "Title", 'Software Engineering', + # bonuses is an int + self.assertRaises(TypeError, Review, 'User', self.comp1, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses=32.40) - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + # state or city are of invalid types + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", 333), pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=(33, "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(TypeError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(TypeError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=(33, True), pay=35.25, bonuses="Bonus") 'Value Errors' - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # invalid date format + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="23-05-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="2022-05-23", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # accepted an offer that wasn't given (offer = False) + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=False, accepted=True, start_date="05-22-2023", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # invalid artings + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=-4, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=10, culture_rat=4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=-4, location=("San Francisco", "California"), pay=35.25, bonuses="Bonus") - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # invalid state + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "Some State"), pay=35.25, bonuses="Bonus") - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # invalid pay + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("San Francisco", "California"), pay=-35.25, bonuses="Bonus") - self.assertRaises(ValueError, Review, self.comp2, "Title", 'Software Engineering', + # invalid city (not alphabetical) + self.assertRaises(ValueError, Review, 'User', self.comp2, "Title", 'Software Engineering', "Position", 4, "M.S.", "Interview", 4, offer=True, accepted=True, start_date="05-23-2022", intern_desc="desc", work_rat=4, culture_rat=4, location=("Some c1t5", "California"), diff --git a/tests/test_user.py b/tests/test_user.py index e69de29..a95bf19 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -0,0 +1,71 @@ +from model import User +import unittest +import bcrypt + +class TestUser(unittest.TestCase): + def setUp(self): + self.valid_email1 = "example@gmail.com" + self.valid_username = "xavier" + self.valid_pic = "http://www.test.com" + self.valid_password = "password1" + + self.user = User( + self.valid_username, + self.valid_email1, + self.valid_password, + self.valid_pic) + + def test_types(self): + #username tests + self.assertRaises(TypeError,User,None,self.valid_email1,self.valid_password,self.valid_pic) + self.assertRaises(TypeError,User,123,self.valid_email1,self.valid_password,self.valid_pic) + self.assertRaises(TypeError,User,[],self.valid_email1,self.valid_password,self.valid_pic) + #email tests + self.assertRaises(TypeError,User,self.valid_username,[],self.valid_password,self.valid_pic) + self.assertRaises(TypeError,User,self.valid_username,None,self.valid_password,self.valid_pic) + self.assertRaises(TypeError,User,self.valid_username,123,self.valid_password,self.valid_pic) + #password tests + self.assertRaises(TypeError,User,self.valid_username,self.valid_email1,None,self.valid_pic) + self.assertRaises(TypeError,User,self.valid_username,self.valid_email1,[],self.valid_pic) + self.assertRaises(TypeError,User,self.valid_username,self.valid_email1,123,self.valid_pic) + #url tests + self.assertRaises(TypeError,User,self.valid_username,self.valid_email1,self.valid_password,None) + self.assertRaises(TypeError,User,self.valid_username,self.valid_email1,self.valid_password,[]) + self.assertRaises(TypeError,User,self.valid_username,self.valid_email1,self.valid_password,123) + + def test_values(self): + #username tests + self.assertRaises(ValueError,User,"tes",self.valid_email1,self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,"",self.valid_email1,self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,"tes",self.valid_email1,self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,"1",self.valid_email1,self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,"__",self.valid_email1,self.valid_password,self.valid_pic) + #email tests + self.assertRaises(ValueError,User,self.valid_username,"inv@test",self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,"email.com",self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,"@test.com",self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,"inv@@@",self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,"t@",self.valid_password,self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,"@.com",self.valid_password,self.valid_pic) + #password tests + self.assertRaises(ValueError,User,self.valid_username,self.valid_email1,"",self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,self.valid_email1,"1",self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,self.valid_email1,"aple",self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,self.valid_email1,"apple",self.valid_pic) + self.assertRaises(ValueError,User,self.valid_username,self.valid_email1,"test12",self.valid_pic) + #profile_pic tests + self.assertRaises(ValueError,User,self.valid_username,self.valid_email1,self.valid_password,"") + + def test_set_profile_pic(self): + self.assertTrue(self.user.set_profile_pic("https://bit.ly/3KF5IT0")) + self.assertFalse(self.user.set_profile_pic("bit.ly/3KF5IT0")) + self.assertEqual(self.user.profile_pic,"https://bit.ly/3KF5IT0") + self.assertTrue(self.user.set_profile_pic("https://www.google.com")) + self.assertEqual(self.user.profile_pic, "https://www.google.com") + + def test_pswd(self): + self.assertTrue(bcrypt.checkpw("password1".encode("utf-8"), + self.user.password)) + + self.assertFalse(bcrypt.checkpw("password".encode("utf-8"), + self.user.password)) \ No newline at end of file