Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ jobs:

- name: Test with pytest
run: |
pytest
pytest --ignore=tests/test_e2e.py
79 changes: 79 additions & 0 deletions .github/workflows/ci_e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: CI for E2E

on:
pull_request:
branches:
- main

jobs:
e2e-test:
runs-on: ubuntu-latest

services:
frontend:
image: node:18
options: --network-alias frontend
env:
PORT: 3000
ports:
- 3000:3000

backend:
image: python:3.9
options: --network-alias backend
env:
PORT: 5000
ports:
- 5000:5000


steps:
# Step 1: Checkout code
- name: Checkout code
uses: actions/checkout@v3

# Step 2: Start FE
- name: Set up and start frontend
working-directory: ./fe_repo
run: |
npm install -g pnpm
pnpm i
pnpm lint
pnpm build
pnpm run dev &

# Step 3: Start BE
- name: Set up and start backend
working-directory: ./be_repo
run: |
pip install -r requirements.txt
python app.py &

# Step 4: Wair for BE and FE Services
- name: Wait for services to be ready
run: |
until curl -s http://localhost:3001; do
echo "Waiting for frontend to be ready..."
sleep 5
done

until curl -s http://localhost:5000; do
echo "Waiting for backend to be ready..."
sleep 5
done

# Step 5: Start E2E Test
- name: Installing package list
run: apt list --installed
- name: Removing previous chrome instances on runner
run: sudo apt purge google-chrome-stable

- name: Installing all necessary packages
run: pip install chromedriver-autoinstaller selenium pyvirtualdisplay
- name: Install xvfb
run: sudo apt-get install xvfb


- name: Run E2E tests
run: |
python ./be_repo/tests/test_e2e.py
81 changes: 71 additions & 10 deletions be_repo/app.py
Original file line number Diff line number Diff line change
@@ -1,12 +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 = ''

# Test MongoDB connection
try:
Expand All @@ -18,21 +35,62 @@
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('/resume_evaluate', methods=['POST'])
@app.route('/login', methods=['POST', 'OPTIONS'])
def login():
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)

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.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 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', '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:
Expand All @@ -47,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:
Expand All @@ -72,4 +133,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)
7 changes: 5 additions & 2 deletions be_repo/modules/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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']
Expand Down
13 changes: 10 additions & 3 deletions be_repo/modules/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions be_repo/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ pymongo==4.10.1
PyPDF2==3.0.1
qdrant_client==1.12.0
tqdm==4.66.2
selenium==4.26.1
google-auth==2.36.0
google-auth-oauthlib==1.2.1
101 changes: 101 additions & 0 deletions be_repo/tests/test_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from selenium import webdriver
import chromedriver_autoinstaller
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.chrome.service import Service
from selenium.webdriver.support import expected_conditions as EC
import time
import os
from pyvirtualdisplay import Display

FRONTEND_URL = "http://localhost:3001"

display = Display(visible=0, size=(800, 800))
display.start()

chromedriver_autoinstaller.install()

chrome_options = webdriver.ChromeOptions()
options = [
"--window-size=1200,1200",
"--ignore-certificate-errors"
"--headless",
]

for option in options:
chrome_options.add_argument(option)

driver = webdriver.Chrome(options=chrome_options)

driver.get(FRONTEND_URL)
wait = WebDriverWait(driver, 20)

try:
wait.until(
lambda driver: driver.execute_script("return document.readyState") == "complete"
)

# Google login
# google_login_button = driver.find_element(By.CSS_SELECTOR, '[aria-labelledby="button-label"]')
# google_login_button.click()

# Upload Resume
upload_div = driver.find_element(By.CSS_SELECTOR, "div[style*='cursor: pointer'][style*='display: flex']")
# upload_div.click()

file_input = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']")))

file_path = os.path.join(os.path.dirname(__file__), 'test_resume.pdf')
file_input.send_keys(file_path)

alert = wait.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 = wait.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()
Loading
Loading