From f3b3c987490d307b96e1707c682bbdf45c71d9bf Mon Sep 17 00:00:00 2001 From: Stanford997 <545255309@qq.com> Date: Mon, 4 Nov 2024 22:25:11 -0500 Subject: [PATCH 01/19] feat: integration test - resume evaluate --- be_repo/tests/test_integration.py | 53 +++++++++++++++++++++++++++++++ test.py | 30 +++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 be_repo/tests/test_integration.py create mode 100644 test.py diff --git a/be_repo/tests/test_integration.py b/be_repo/tests/test_integration.py new file mode 100644 index 000000000..9f744300b --- /dev/null +++ b/be_repo/tests/test_integration.py @@ -0,0 +1,53 @@ +""" +Flask Integration Test. +""" +import pytest +from flask import Flask +from app import app +from configs.database import get_resume_database + + +@pytest.fixture +def client(): + # Create testing client + app.config['TESTING'] = True + app.config['DEBUG'] = False + client = app.test_client() + + with app.app_context(): + database = get_resume_database() + resume_collection = database.get_collection("resumes") + resume_collection.delete_many({"user_id": "test"}) + yield client, resume_collection + resume_collection.delete_many({"user_id": "test"}) + + +def test_resume_evaluate(client): + client, resume_collection = client + + resume_collection.insert_one({ + 'user_id': 'test', + 'resume_text': 'Sample resume content for evaluation.' + }) + + response = client.post('/resume_evaluate', data={'user_id': 'test'}) + assert response.status_code == 200 + json_data = response.get_json() + assert 'analysis' in json_data + + +def test_resume_evaluate_with_JD(client): + client, resume_collection = client + + resume_collection.insert_one({ + 'user_id': 'test', + 'resume_text': 'Sample resume content for JD evaluation.' + }) + + response = client.post('/resume_evaluate_with_JD', data={ + 'user_id': 'test', + 'jd_text': 'Sample job description text for matching.' + }) + assert response.status_code == 200 + json_data = response.get_json() + assert 'analysis' in json_data diff --git a/test.py b/test.py new file mode 100644 index 000000000..59b31d68e --- /dev/null +++ b/test.py @@ -0,0 +1,30 @@ +@patch('fetch_file') +def test_file_to_string(file, app): + file = 'mock_file' # File content + file_to_text = file_to_string(text) + + assert file_to_text == 'abcdefg' + +@patch('de_encryption_1') +def test_de_encryption(text, app): + text = 'abcdefg' # Set up the application context + encrypted_text = encryption_1(text) + + assert encrypted_text == 'gfedcba' + + original_text = decryption_1(encrypted_text) + + assert encrypted_text == 'abcdefg' + + +@patch('de_encryption_2') +def test_de_encryption(text, app): + text = 'abcdefg' # Set up the application context + encrypted_text = encryption_2(text) + + assert encrypted_text == '123456' + + original_text = decryption_2(encrypted_text) + + assert encrypted_text == 'abcdefg' + From 17e1d2caa7c0365a249fa16750ea9c4462968f26 Mon Sep 17 00:00:00 2001 From: Stanford997 <545255309@qq.com> Date: Mon, 4 Nov 2024 22:26:32 -0500 Subject: [PATCH 02/19] remove unnecessary file --- test.py | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index 59b31d68e..000000000 --- a/test.py +++ /dev/null @@ -1,30 +0,0 @@ -@patch('fetch_file') -def test_file_to_string(file, app): - file = 'mock_file' # File content - file_to_text = file_to_string(text) - - assert file_to_text == 'abcdefg' - -@patch('de_encryption_1') -def test_de_encryption(text, app): - text = 'abcdefg' # Set up the application context - encrypted_text = encryption_1(text) - - assert encrypted_text == 'gfedcba' - - original_text = decryption_1(encrypted_text) - - assert encrypted_text == 'abcdefg' - - -@patch('de_encryption_2') -def test_de_encryption(text, app): - text = 'abcdefg' # Set up the application context - encrypted_text = encryption_2(text) - - assert encrypted_text == '123456' - - original_text = decryption_2(encrypted_text) - - assert encrypted_text == 'abcdefg' - From 18ad2e515bfd94bacfe96177afc825ff01454939 Mon Sep 17 00:00:00 2001 From: zihan zhou <95243748+andyasdd1@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:03:16 -0500 Subject: [PATCH 03/19] Update: app.py Add in login methods for backend --- be_repo/app.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/be_repo/app.py b/be_repo/app.py index f89744d4d..9796ca5cf 100644 --- a/be_repo/app.py +++ b/be_repo/app.py @@ -4,10 +4,14 @@ from configs.database import get_resume_database from modules.evaluator import evaluate_resume, evaluate_resume_with_jd from modules.upload import upload_parse_resume +from google.oauth2 import id_token +from google.auth.transport import requests as google_requests app = Flask(__name__) CORS(app) +GOOGLE_CLIENT_ID = '788436279160-4vqq0leogg079merpfgfgnnimlvo8mhh.apps.googleusercontent.com' + # Test MongoDB connection try: database = get_resume_database() @@ -26,6 +30,35 @@ def upload_resume(): # Vector the resume text return upload_parse_resume(request, resume_collection) +@app.route('/login', methods=['POST']) +def login(): + token = request.json.get('id_token') + print(token) + if not token: + return jsonify({'status': 'error', 'message': 'ID token is missing'}), 400 + + try: + # Verify the token with Google + idinfo = id_token.verify_oauth2_token(token, google_requests.Request(), GOOGLE_CLIENT_ID) + + if idinfo['aud'] != GOOGLE_CLIENT_ID: + return jsonify({'status': 'error', 'message': 'Token audience mismatch'}), 400 + + # Token is valid, extract user information + userid = idinfo['sub'] + email = idinfo['email'] + name = idinfo.get('name', 'No name available') + + # Create a session or JWT for the user + #session['user'] = {'userid': userid, 'email': email, 'name': name} + + # Respond with success and optional user data + return jsonify({'status': 'success', 'email': email, 'name': name}), 200 + + except ValueError: + # Token verification failed + print(f"Error verifying token: {str(e)}") + return jsonify({'status': 'error', 'message': 'Invalid ID token'}), 400 @app.route('/resume_evaluate', methods=['POST']) def resume_evaluate(): From a8605c8500b290fd59d802c1df721be9b11fa591 Mon Sep 17 00:00:00 2001 From: zihan zhou <95243748+andyasdd1@users.noreply.github.com> Date: Wed, 6 Nov 2024 17:23:52 -0500 Subject: [PATCH 04/19] Update app.py Changes: fixed login issue, partial implemented session control --- be_repo/app.py | 60 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/be_repo/app.py b/be_repo/app.py index 9796ca5cf..aa7d97952 100644 --- a/be_repo/app.py +++ b/be_repo/app.py @@ -1,16 +1,29 @@ -from flask import Flask, request, jsonify +from flask import Flask, request, jsonify, session from flask_cors import CORS +from datetime import timedelta from configs.database import get_resume_database from modules.evaluator import evaluate_resume, evaluate_resume_with_jd from modules.upload import upload_parse_resume from google.oauth2 import id_token from google.auth.transport import requests as google_requests +import secrets + +# Generate a secure random secret key +secret_key = secrets.token_hex(32) # Generates a 64-character hexadecimal string app = Flask(__name__) -CORS(app) +CORS(app, supports_credentials=True) +app.secret_key = secret_key + +app.config.update( + SESSION_COOKIE_SECURE=False, # Set to True if using HTTPS + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE='Lax', + PERMANENT_SESSION_LIFETIME=timedelta(minutes=30), +) -GOOGLE_CLIENT_ID = '788436279160-4vqq0leogg079merpfgfgnnimlvo8mhh.apps.googleusercontent.com' +GOOGLE_CLIENT_ID = '120137358324-l62fq2hlj9r31evvitg55rcl4rf21udd.apps.googleusercontent.com' # Test MongoDB connection try: @@ -22,21 +35,28 @@ raise Exception("Unable to find the document due to the following error: ", e) -@app.route('/upload', methods=['POST']) -def upload_resume(): +@app.route('/upload', methods=['POST', 'OPTIONS']) +def upload_resume(): + if request.method == 'OPTIONS': + # Allows the preflight request to succeed + return jsonify({'status': 'OK'}), 200 + user_id = request.form.get('user_id') if not user_id: return jsonify({"error": "No user ID provided."}), 400 # Vector the resume text return upload_parse_resume(request, resume_collection) -@app.route('/login', methods=['POST']) +@app.route('/login', methods=['POST', 'OPTIONS']) def login(): - token = request.json.get('id_token') - print(token) + if request.method == 'OPTIONS': + # Allows the preflight request to succeed + return jsonify({'status': 'OK'}), 200 + + token = request.form.get('access_token') if not token: return jsonify({'status': 'error', 'message': 'ID token is missing'}), 400 - + try: # Verify the token with Google idinfo = id_token.verify_oauth2_token(token, google_requests.Request(), GOOGLE_CLIENT_ID) @@ -50,22 +70,27 @@ def login(): name = idinfo.get('name', 'No name available') # Create a session or JWT for the user - #session['user'] = {'userid': userid, 'email': email, 'name': name} + session.permanent = True + session['user'] = {'userid': userid, 'email': email, 'name': name} # Respond with success and optional user data return jsonify({'status': 'success', 'email': email, 'name': name}), 200 - - except ValueError: + + except ValueError as e: # Token verification failed print(f"Error verifying token: {str(e)}") return jsonify({'status': 'error', 'message': 'Invalid ID token'}), 400 -@app.route('/resume_evaluate', methods=['POST']) +@app.route('/resume_evaluate', methods=['POST', 'OPTIONS']) def resume_evaluate(): + if request.method == 'OPTIONS': + # Allows the preflight request to succeed + return jsonify({'status': 'OK'}), 200 + user_id = request.form.get('user_id') if not user_id: return jsonify({"error": "No user ID provided."}), 400 - + # Load resume from database resume = resume_collection.find_one({"user_id": user_id}) if not resume: @@ -80,11 +105,14 @@ def resume_evaluate(): return jsonify({"analysis": analysis_result}), 200 -@app.route('/resume_evaluate_with_JD', methods=['POST']) +@app.route('/resume_evaluate_with_JD', methods=['POST', 'OPTIONS']) def resume_evaluate_with_JD(): + if request.method == 'OPTIONS': + # Allows the preflight request to succeed + return jsonify({'status': 'OK'}), 200 + user_id = request.form.get('user_id') jd_text = request.form.get('jd_text') - if not user_id: return jsonify({"error": "No user ID provided."}), 400 if not jd_text: From 38588819cd8383d4e9443478c86a8a481411f217 Mon Sep 17 00:00:00 2001 From: Stanford997 <545255309@qq.com> Date: Wed, 6 Nov 2024 18:28:04 -0500 Subject: [PATCH 05/19] feat: new logic for compute correlated score --- be_repo/app.py | 2 +- be_repo/modules/evaluator.py | 7 +++++-- be_repo/modules/upload.py | 13 ++++++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/be_repo/app.py b/be_repo/app.py index f89744d4d..fad5bcf34 100644 --- a/be_repo/app.py +++ b/be_repo/app.py @@ -72,4 +72,4 @@ def resume_evaluate_with_JD(): if __name__ == '__main__': - app.run(host='0.0.0.0', debug=True) + app.run(host='0.0.0.0', debug=True, port=5000) diff --git a/be_repo/modules/evaluator.py b/be_repo/modules/evaluator.py index 7dc253781..39d46d4a1 100644 --- a/be_repo/modules/evaluator.py +++ b/be_repo/modules/evaluator.py @@ -123,7 +123,10 @@ def extract_scores_and_explanation(evaluation): def compute_correlated_score(score, correlation): - correlated_score = [score[i] * correlation[i] * 2 for i in range(4)] + max_ranges = [SCORING_CRITERIA['Education'], SCORING_CRITERIA['Project and Work Experience'], + SCORING_CRITERIA['Skills and Certifications'], SCORING_CRITERIA['Soft Skills']] + correlated_score = [min(int(score[i] * correlation[i] ** 2 * 2), max_ranges[i]) for i in range(4)] + return correlated_score @@ -232,7 +235,7 @@ def evaluate_resume_with_jd(resume_text, jd_text): # Extract correlation scores and original scores into arrays and compute correlated_score correlation_array = list(correlation['correlation'].values()) scores_array = list(evaluated_resume_with_jd['scores'].values()) - correlated_score = compute_correlated_score(correlation_array, scores_array) + correlated_score = compute_correlated_score(scores_array, correlation_array) # Update the scores and explanations in the evaluated resume based on correlation analysis fields_to_replace = ['Education', 'Project and Work Experience', 'Skills and Certifications', 'Soft Skills'] diff --git a/be_repo/modules/upload.py b/be_repo/modules/upload.py index 4c1e0ce77..0ac178ccc 100644 --- a/be_repo/modules/upload.py +++ b/be_repo/modules/upload.py @@ -30,11 +30,18 @@ def upload_parse_resume(request, resume_collection): "user_id": user_id, "resume_text": resume_text } - result = resume_collection.insert_one(new_resume) + result = resume_collection.replace_one({"user_id": user_id}, new_resume, upsert=True) + + if result.upserted_id: + resume_id = str(result.upserted_id) + message = "File successfully uploaded and parsed" + else: + resume_id = str(resume_collection.find_one({"user_id": user_id})["_id"]) + message = "Existing resume updated successfully" return jsonify({ - "message": "File successfully uploaded and parsed", - "resume_id": str(result.inserted_id) + "message": message, + "resume_id": resume_id }), 200 else: return jsonify({"error": "Invalid file format, only PDF is allowed"}), 400 From 1eb58fe19fdefb96206376e8ae4c26d9a1d74964 Mon Sep 17 00:00:00 2001 From: Stanford997 <545255309@qq.com> Date: Wed, 6 Nov 2024 18:30:03 -0500 Subject: [PATCH 06/19] feat: e2e test --- be_repo/tests/test_e2e.py | 85 +++++++++++++++++++++++++++++++ be_repo/tests/test_resume.pdf | Bin 0 -> 18944 bytes fe_repo/src/pages/ChatBox.tsx | 93 +++++++++++++++++----------------- 3 files changed, 131 insertions(+), 47 deletions(-) create mode 100644 be_repo/tests/test_e2e.py create mode 100644 be_repo/tests/test_resume.pdf diff --git a/be_repo/tests/test_e2e.py b/be_repo/tests/test_e2e.py new file mode 100644 index 000000000..60f6fdd31 --- /dev/null +++ b/be_repo/tests/test_e2e.py @@ -0,0 +1,85 @@ +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support import expected_conditions as EC +import time + +FRONTEND_URL = "http://localhost:3000" +API_URL = "http://127.0.0.1:5000" + +options = Options() +options.add_argument("--headless") +driver = webdriver.Chrome(options=options) +driver.get(FRONTEND_URL) +wait = WebDriverWait(driver, 10) + +try: + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CLASS_NAME, "nsm7Bb-HzV7m-LgbsSe-MJoBVe")) + ) + + # Google login + google_login_button = driver.find_element(By.CLASS_NAME, "nsm7Bb-HzV7m-LgbsSe") + # google_login_button.click() + + # Upload Resume + upload_div = driver.find_element(By.CSS_SELECTOR, "div[style*='cursor: pointer'][style*='display: flex']") + # upload_div.click() + + wait = WebDriverWait(driver, 10) + file_input = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']"))) + + file_path = "/Users/caozhen/PycharmProjects/seprojects-cs673a2f24_team5/be_repo/tests/test_resume.pdf" + file_input.send_keys(file_path) + + alert = WebDriverWait(driver, 10).until(EC.alert_is_present()) + alert_text = alert.text + assert "Resume uploaded successfully" in alert_text + alert.accept() + + # Analyze resume + analyze_button = driver.find_element(By.CLASS_NAME, "cursor-pointer") + analyze_button.click() + + submit_button = driver.find_element(By.CLASS_NAME, "mt-6") + submit_button.click() + + time.sleep(20) + + keywords = [ + "Analysis Result", + "Consistency and Chronology", + "Education", + "Project and Work Experience", + "Resume Structure and Presentation", + "Skills and Certifications", + "Soft Skills" + ] + + elements = driver.find_elements(By.CLASS_NAME, "font-bold") + content_text = " ".join([element.text for element in elements]) + for keyword in keywords: + count = content_text.count(keyword) + assert count == 1, f"'{keyword}' does not appear exactly twice in the content (found {count} times)" + + # Analyze resume with JD + analyze_button.click() + textarea = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CSS_SELECTOR, "textarea.w-full.h-40.p-4.border-2")) + ) + textarea.send_keys("Sample job description text") + submit_button = driver.find_element(By.CLASS_NAME, "mt-6") + submit_button.click() + + time.sleep(20) + + elements = driver.find_elements(By.CLASS_NAME, "font-bold") + content_text = " ".join([element.text for element in elements]) + for keyword in keywords: + count = content_text.count(keyword) + assert count == 2, f"'{keyword}' does not appear exactly twice in the content (found {count} times)" + +finally: + driver.quit() diff --git a/be_repo/tests/test_resume.pdf b/be_repo/tests/test_resume.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bc202e798a2c926034af7f66c0b17de227c71935 GIT binary patch literal 18944 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 literal 0 HcmV?d00001 diff --git a/fe_repo/src/pages/ChatBox.tsx b/fe_repo/src/pages/ChatBox.tsx index 2a56149e0..456a36aee 100644 --- a/fe_repo/src/pages/ChatBox.tsx +++ b/fe_repo/src/pages/ChatBox.tsx @@ -63,54 +63,53 @@ export const ChatBox = ({ onSendMessage, onAnalyze }: IChatBoxProps) => { } {/* icon row */ } -
-
{ - // Upload files - const handleFiles = (event: Event) => { - const files = (event.target as HTMLInputElement)?.files; - if (!files || files.length === 0) return; - const file = files[0]; - if (file) { - // upload if file is valid - uploadFile(file); - } - } - - const input = document.createElement("input"); - input.type = 'file'; - input.accept = '.pdf'; - input.onchange = handleFiles; - input.click(); - }} - > - +
+
{ + document.getElementById('fileInput').click(); + }} + > + + { + const files = event.target.files; + if (!files || files.length === 0) return; + const file = files[0]; + if (file) { + uploadFile(file); + } + }} + /> +
+ + {/*🛈 /!* Example icon - you can use actual icons here *!/*/} + {/*💼 /!* Example icon for interview *!/*/} +
+ {/* input row */} +
+