Merge pull request #355 from blackms/v3
Add Unit Tests for LinkedInJobManager
This commit is contained in:
commit
5568931234
10 changed files with 706 additions and 0 deletions
5
pytest.ini
Normal file
5
pytest.ini
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[pytest]
|
||||
minversion = 6.0
|
||||
addopts = --strict-markers --tb=short --cov=src --cov-report=term-missing
|
||||
testpaths =
|
||||
tests
|
||||
|
|
@ -24,3 +24,4 @@ jsonschema-specifications==2023.12.1
|
|||
httpx~=0.27.2
|
||||
python-dotenv~=1.0.1
|
||||
PyYAML~=6.0.2
|
||||
pytest>=8.3.3
|
||||
|
|
|
|||
|
|
@ -76,6 +76,20 @@ class LinkedInEasyApplier:
|
|||
logger.error("Failed to return to job page after %d attempts. Cannot apply for the job.", max_attempts)
|
||||
raise Exception(
|
||||
f"Redirected to LinkedIn Premium page and failed to return after {max_attempts} attempts. Job application aborted.")
|
||||
|
||||
def apply_to_job(self, job: Any) -> None:
|
||||
"""
|
||||
Starts the process of applying to a job.
|
||||
:param job: A job object with the job details.
|
||||
:return: None
|
||||
"""
|
||||
logger.debug(f"Applying to job: {job}")
|
||||
try:
|
||||
self.job_apply(job)
|
||||
logger.info(f"Successfully applied to job: {job.title}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply to job: {job.title}, error: {str(e)}")
|
||||
raise e
|
||||
|
||||
def job_apply(self, job: Any):
|
||||
logger.debug("Starting job application for job: %s", job)
|
||||
|
|
|
|||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
153
tests/test_job_application_profile.py
Normal file
153
tests/test_job_application_profile.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import pytest
|
||||
from src.job_application_profile import JobApplicationProfile
|
||||
|
||||
@pytest.fixture
|
||||
def valid_yaml():
|
||||
"""Valid YAML string for initializing JobApplicationProfile."""
|
||||
return """
|
||||
self_identification:
|
||||
gender: Male
|
||||
pronouns: He/Him
|
||||
veteran: No
|
||||
disability: No
|
||||
ethnicity: Asian
|
||||
legal_authorization:
|
||||
eu_work_authorization: "Yes"
|
||||
us_work_authorization: "Yes"
|
||||
requires_us_visa: "No"
|
||||
legally_allowed_to_work_in_us: "Yes"
|
||||
requires_us_sponsorship: "No"
|
||||
requires_eu_visa: "No"
|
||||
legally_allowed_to_work_in_eu: "Yes"
|
||||
requires_eu_sponsorship: "No"
|
||||
work_preferences:
|
||||
remote_work: "Yes"
|
||||
in_person_work: "No"
|
||||
open_to_relocation: "Yes"
|
||||
willing_to_complete_assessments: "Yes"
|
||||
willing_to_undergo_drug_tests: "Yes"
|
||||
willing_to_undergo_background_checks: "Yes"
|
||||
availability:
|
||||
notice_period: "2 weeks"
|
||||
salary_expectations:
|
||||
salary_range_usd: "80000-120000"
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def missing_field_yaml():
|
||||
"""YAML string missing a required field (self_identification)."""
|
||||
return """
|
||||
legal_authorization:
|
||||
eu_work_authorization: "Yes"
|
||||
us_work_authorization: "Yes"
|
||||
requires_us_visa: "No"
|
||||
legally_allowed_to_work_in_us: "Yes"
|
||||
requires_us_sponsorship: "No"
|
||||
requires_eu_visa: "No"
|
||||
legally_allowed_to_work_in_eu: "Yes"
|
||||
requires_eu_sponsorship: "No"
|
||||
work_preferences:
|
||||
remote_work: "Yes"
|
||||
in_person_work: "No"
|
||||
open_to_relocation: "Yes"
|
||||
willing_to_complete_assessments: "Yes"
|
||||
willing_to_undergo_drug_tests: "Yes"
|
||||
willing_to_undergo_background_checks: "Yes"
|
||||
availability:
|
||||
notice_period: "2 weeks"
|
||||
salary_expectations:
|
||||
salary_range_usd: "80000-120000"
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def invalid_type_yaml():
|
||||
"""YAML string with an invalid type for a field."""
|
||||
return """
|
||||
self_identification:
|
||||
gender: Male
|
||||
pronouns: He/Him
|
||||
veteran: No
|
||||
disability: No
|
||||
ethnicity: Asian
|
||||
legal_authorization:
|
||||
eu_work_authorization: "Yes"
|
||||
us_work_authorization: "Yes"
|
||||
requires_us_visa: "No"
|
||||
legally_allowed_to_work_in_us: "Yes"
|
||||
requires_us_sponsorship: "No"
|
||||
requires_eu_visa: "No"
|
||||
legally_allowed_to_work_in_eu: "Yes"
|
||||
requires_eu_sponsorship: "No"
|
||||
work_preferences:
|
||||
remote_work: 12345 # Invalid type, expecting a string
|
||||
in_person_work: "No"
|
||||
open_to_relocation: "Yes"
|
||||
willing_to_complete_assessments: "Yes"
|
||||
willing_to_undergo_drug_tests: "Yes"
|
||||
willing_to_undergo_background_checks: "Yes"
|
||||
availability:
|
||||
notice_period: "2 weeks"
|
||||
salary_expectations:
|
||||
salary_range_usd: "80000-120000"
|
||||
"""
|
||||
|
||||
def test_initialize_with_valid_yaml(valid_yaml):
|
||||
"""Test initializing JobApplicationProfile with valid YAML."""
|
||||
profile = JobApplicationProfile(valid_yaml)
|
||||
|
||||
# Check that the profile fields are correctly initialized
|
||||
assert profile.self_identification.gender == "Male"
|
||||
assert profile.self_identification.pronouns == "He/Him"
|
||||
assert profile.legal_authorization.eu_work_authorization == "Yes"
|
||||
assert profile.work_preferences.remote_work == "Yes"
|
||||
assert profile.availability.notice_period == "2 weeks"
|
||||
assert profile.salary_expectations.salary_range_usd == "80000-120000"
|
||||
|
||||
def test_initialize_with_missing_field(missing_field_yaml):
|
||||
"""Test initializing JobApplicationProfile with missing required fields."""
|
||||
with pytest.raises(KeyError) as excinfo:
|
||||
JobApplicationProfile(missing_field_yaml)
|
||||
assert "self_identification" in str(excinfo.value)
|
||||
|
||||
def test_initialize_with_invalid_yaml():
|
||||
"""Test initializing JobApplicationProfile with invalid YAML."""
|
||||
invalid_yaml_str = """
|
||||
self_identification:
|
||||
gender: Male
|
||||
pronouns: He/Him
|
||||
veteran: No
|
||||
disability: No
|
||||
ethnicity: Asian
|
||||
legal_authorization:
|
||||
eu_work_authorization: "Yes"
|
||||
us_work_authorization: "Yes"
|
||||
requires_us_visa: "No"
|
||||
legally_allowed_to_work_in_us: "Yes"
|
||||
requires_us_sponsorship: "No"
|
||||
requires_eu_visa: "No"
|
||||
legally_allowed_to_work_in_eu: "Yes"
|
||||
requires_eu_sponsorship: "No"
|
||||
work_preferences:
|
||||
remote_work: "Yes"
|
||||
in_person_work: "No"
|
||||
availability:
|
||||
notice_period: "2 weeks"
|
||||
salary_expectations:
|
||||
salary_range_usd: "80000-120000"
|
||||
""" # Missing fields in work_preferences
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
JobApplicationProfile(invalid_yaml_str)
|
||||
|
||||
def test_str_representation(valid_yaml):
|
||||
"""Test the string representation of JobApplicationProfile."""
|
||||
profile = JobApplicationProfile(valid_yaml)
|
||||
profile_str = str(profile)
|
||||
|
||||
assert "Self Identification:" in profile_str
|
||||
assert "Legal Authorization:" in profile_str
|
||||
assert "Work Preferences:" in profile_str
|
||||
assert "Availability:" in profile_str
|
||||
assert "Salary Expectations:" in profile_str
|
||||
assert "Male" in profile_str
|
||||
assert "80000-120000" in profile_str
|
||||
158
tests/test_linkedIn_authenticator.py
Normal file
158
tests/test_linkedIn_authenticator.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import pytest
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from src.linkedIn_authenticator import LinkedInAuthenticator
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_driver(mocker):
|
||||
"""Fixture to mock the Selenium WebDriver."""
|
||||
return mocker.Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticator(mock_driver):
|
||||
"""Fixture to initialize LinkedInAuthenticator with a mocked driver."""
|
||||
return LinkedInAuthenticator(mock_driver)
|
||||
|
||||
|
||||
def test_set_secrets(authenticator):
|
||||
"""Test setting secrets (email, password)."""
|
||||
authenticator.set_secrets("test@example.com", "password123")
|
||||
assert authenticator.email == "test@example.com"
|
||||
assert authenticator.password == "password123"
|
||||
|
||||
|
||||
def test_start_logged_in(mocker, authenticator):
|
||||
"""Test starting LinkedIn when already logged in."""
|
||||
mocker.patch.object(authenticator, 'is_logged_in', return_value=True)
|
||||
mocker.patch.object(authenticator.driver, 'get')
|
||||
mocker.patch("time.sleep") # Avoid waiting during the test
|
||||
|
||||
authenticator.start()
|
||||
|
||||
authenticator.driver.get.assert_called_with('https://www.linkedin.com/feed')
|
||||
authenticator.is_logged_in.assert_called_once()
|
||||
assert authenticator.driver.get.call_count == 1
|
||||
|
||||
|
||||
def test_start_not_logged_in(mocker, authenticator):
|
||||
"""Test starting LinkedIn when not logged in."""
|
||||
mocker.patch.object(authenticator, 'is_logged_in', return_value=False)
|
||||
mocker.patch.object(authenticator, 'handle_login')
|
||||
mocker.patch.object(authenticator.driver, 'get')
|
||||
mocker.patch("time.sleep")
|
||||
|
||||
authenticator.start()
|
||||
|
||||
authenticator.driver.get.assert_called_with('https://www.linkedin.com/feed')
|
||||
authenticator.handle_login.assert_called_once()
|
||||
|
||||
|
||||
def test_handle_login(mocker, authenticator):
|
||||
"""Test handling the LinkedIn login process."""
|
||||
mocker.patch.object(authenticator.driver, 'get')
|
||||
mocker.patch.object(authenticator, 'enter_credentials')
|
||||
mocker.patch.object(authenticator, 'submit_login_form')
|
||||
mocker.patch.object(authenticator, 'handle_security_check')
|
||||
|
||||
# Mock current_url as a regular return value, not PropertyMock
|
||||
mocker.patch.object(authenticator.driver, 'current_url', return_value='https://www.linkedin.com/login')
|
||||
|
||||
authenticator.handle_login()
|
||||
|
||||
authenticator.driver.get.assert_called_with('https://www.linkedin.com/login')
|
||||
authenticator.enter_credentials.assert_called_once()
|
||||
authenticator.submit_login_form.assert_called_once()
|
||||
authenticator.handle_security_check.assert_called_once()
|
||||
|
||||
|
||||
def test_enter_credentials_success(mocker, authenticator):
|
||||
"""Test entering credentials."""
|
||||
email_mock = mocker.Mock()
|
||||
password_mock = mocker.Mock()
|
||||
|
||||
mocker.patch.object(WebDriverWait, 'until', return_value=email_mock)
|
||||
mocker.patch.object(authenticator.driver, 'find_element', return_value=password_mock)
|
||||
|
||||
authenticator.set_secrets("test@example.com", "password123")
|
||||
authenticator.enter_credentials()
|
||||
|
||||
email_mock.send_keys.assert_called_once_with("test@example.com")
|
||||
password_mock.send_keys.assert_called_once_with("password123")
|
||||
|
||||
|
||||
def test_enter_credentials_timeout(mocker, authenticator):
|
||||
"""Test entering credentials with a TimeoutException."""
|
||||
mocker.patch.object(WebDriverWait, 'until', side_effect=TimeoutException)
|
||||
|
||||
authenticator.set_secrets("test@example.com", "password123")
|
||||
|
||||
authenticator.enter_credentials()
|
||||
|
||||
authenticator.driver.find_element.assert_not_called() # Password input should not be accessed if email fails
|
||||
|
||||
|
||||
def test_submit_login_form_success(mocker, authenticator):
|
||||
"""Test submitting the login form."""
|
||||
login_button_mock = mocker.Mock()
|
||||
mocker.patch.object(authenticator.driver, 'find_element', return_value=login_button_mock)
|
||||
|
||||
authenticator.submit_login_form()
|
||||
|
||||
login_button_mock.click.assert_called_once()
|
||||
|
||||
|
||||
def test_submit_login_form_no_button(mocker, authenticator):
|
||||
"""Test submitting the login form when the login button is not found."""
|
||||
mocker.patch.object(authenticator.driver, 'find_element', side_effect=NoSuchElementException)
|
||||
|
||||
authenticator.submit_login_form()
|
||||
|
||||
authenticator.driver.find_element.assert_called_once_with(By.XPATH, '//button[@type="submit"]')
|
||||
|
||||
|
||||
def test_is_logged_in_true(mocker, authenticator):
|
||||
"""Test if the user is logged in."""
|
||||
buttons_mock = mocker.Mock()
|
||||
buttons_mock.text = "Start a post"
|
||||
mocker.patch.object(WebDriverWait, 'until')
|
||||
mocker.patch.object(authenticator.driver, 'find_elements', return_value=[buttons_mock])
|
||||
|
||||
assert authenticator.is_logged_in() is True
|
||||
|
||||
|
||||
def test_is_logged_in_false(mocker, authenticator):
|
||||
"""Test if the user is not logged in."""
|
||||
mocker.patch.object(WebDriverWait, 'until')
|
||||
mocker.patch.object(authenticator.driver, 'find_elements', return_value=[])
|
||||
|
||||
assert authenticator.is_logged_in() is False
|
||||
|
||||
|
||||
def test_handle_security_check_success(mocker, authenticator):
|
||||
"""Test handling security check successfully."""
|
||||
mocker.patch.object(WebDriverWait, 'until', side_effect=[
|
||||
mocker.Mock(), # Security checkpoint detection
|
||||
mocker.Mock() # Security check completion
|
||||
])
|
||||
|
||||
authenticator.handle_security_check()
|
||||
|
||||
# Verify WebDriverWait is called with EC.url_contains for both the challenge and feed
|
||||
WebDriverWait(authenticator.driver, 10).until.assert_any_call(mocker.ANY)
|
||||
WebDriverWait(authenticator.driver, 300).until.assert_any_call(mocker.ANY)
|
||||
|
||||
|
||||
|
||||
def test_handle_security_check_timeout(mocker, authenticator):
|
||||
"""Test handling security check timeout."""
|
||||
mocker.patch.object(WebDriverWait, 'until', side_effect=TimeoutException)
|
||||
|
||||
authenticator.handle_security_check()
|
||||
|
||||
# Verify WebDriverWait is called with EC.url_contains for the challenge
|
||||
WebDriverWait(authenticator.driver, 10).until.assert_any_call(mocker.ANY)
|
||||
|
||||
14
tests/test_linkedIn_bot_facade.py
Normal file
14
tests/test_linkedIn_bot_facade.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import pytest
|
||||
# from src.linkedIn_job_manager import JobManager
|
||||
|
||||
@pytest.fixture
|
||||
def job_manager():
|
||||
"""Fixture for JobManager."""
|
||||
return None # Replace with valid instance or mock later
|
||||
|
||||
def test_bot_functionality(job_manager):
|
||||
"""Test LinkedIn bot facade."""
|
||||
# Example: test job manager interacts with the bot facade correctly
|
||||
job = {"title": "Software Engineer"}
|
||||
# job_manager.some_method_to_apply(job)
|
||||
assert job is not None # Placeholder for actual test
|
||||
97
tests/test_linkedIn_easy_applier.py
Normal file
97
tests/test_linkedIn_easy_applier.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import pytest
|
||||
from unittest import mock
|
||||
from src.linkedIn_easy_applier import LinkedInEasyApplier
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_driver():
|
||||
"""Fixture to mock Selenium WebDriver."""
|
||||
return mock.Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_gpt_answerer():
|
||||
"""Fixture to mock GPT Answerer."""
|
||||
return mock.Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_resume_generator_manager():
|
||||
"""Fixture to mock Resume Generator Manager."""
|
||||
return mock.Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def easy_applier(mock_driver, mock_gpt_answerer, mock_resume_generator_manager):
|
||||
"""Fixture to initialize LinkedInEasyApplier with mocks."""
|
||||
return LinkedInEasyApplier(
|
||||
driver=mock_driver,
|
||||
resume_dir="/path/to/resume",
|
||||
set_old_answers=[('Question 1', 'Answer 1', 'Type 1')],
|
||||
gpt_answerer=mock_gpt_answerer,
|
||||
resume_generator_manager=mock_resume_generator_manager
|
||||
)
|
||||
|
||||
|
||||
def test_initialization(mocker, easy_applier):
|
||||
"""Test that LinkedInEasyApplier is initialized correctly."""
|
||||
# Mock os.path.exists to return True
|
||||
mocker.patch('os.path.exists', return_value=True)
|
||||
|
||||
easy_applier = LinkedInEasyApplier(
|
||||
driver=mocker.Mock(),
|
||||
resume_dir="/path/to/resume",
|
||||
set_old_answers=[('Question 1', 'Answer 1', 'Type 1')],
|
||||
gpt_answerer=mocker.Mock(),
|
||||
resume_generator_manager=mocker.Mock()
|
||||
)
|
||||
|
||||
assert easy_applier.resume_path == "/path/to/resume"
|
||||
assert len(easy_applier.set_old_answers) == 1
|
||||
assert easy_applier.gpt_answerer is not None
|
||||
assert easy_applier.resume_generator_manager is not None
|
||||
|
||||
|
||||
def test_apply_to_job_success(mocker, easy_applier):
|
||||
"""Test successfully applying to a job."""
|
||||
mock_job = mock.Mock()
|
||||
|
||||
# Mock job_apply so we don't actually try to apply
|
||||
mocker.patch.object(easy_applier, 'job_apply')
|
||||
|
||||
easy_applier.apply_to_job(mock_job)
|
||||
easy_applier.job_apply.assert_called_once_with(mock_job)
|
||||
|
||||
|
||||
def test_apply_to_job_failure(mocker, easy_applier):
|
||||
"""Test failure while applying to a job."""
|
||||
mock_job = mock.Mock()
|
||||
mocker.patch.object(easy_applier, 'job_apply',
|
||||
side_effect=Exception("Test error"))
|
||||
|
||||
with pytest.raises(Exception, match="Test error"):
|
||||
easy_applier.apply_to_job(mock_job)
|
||||
|
||||
easy_applier.job_apply.assert_called_once_with(mock_job)
|
||||
|
||||
|
||||
def test_check_for_premium_redirect_no_redirect(mocker, easy_applier):
|
||||
"""Test that check_for_premium_redirect works when there's no redirect."""
|
||||
mock_job = mock.Mock()
|
||||
easy_applier.driver.current_url = "https://www.linkedin.com/jobs/view/1234"
|
||||
|
||||
easy_applier.check_for_premium_redirect(mock_job)
|
||||
easy_applier.driver.get.assert_not_called()
|
||||
|
||||
|
||||
def test_check_for_premium_redirect_with_redirect(mocker, easy_applier):
|
||||
"""Test that check_for_premium_redirect handles LinkedIn Premium redirects."""
|
||||
mock_job = mock.Mock()
|
||||
easy_applier.driver.current_url = "https://www.linkedin.com/premium"
|
||||
mock_job.link = "https://www.linkedin.com/jobs/view/1234"
|
||||
|
||||
with pytest.raises(Exception, match="Redirected to LinkedIn Premium page and failed to return"):
|
||||
easy_applier.check_for_premium_redirect(mock_job)
|
||||
|
||||
# Verify that it attempted to return to the job page 3 times
|
||||
assert easy_applier.driver.get.call_count == 3
|
||||
168
tests/test_linkedIn_job_manager.py
Normal file
168
tests/test_linkedIn_job_manager.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
from src.job import Job
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
import os
|
||||
import pytest
|
||||
from src.linkedIn_job_manager import LinkedInJobManager
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def job_manager(mocker):
|
||||
"""Fixture to create a LinkedInJobManager instance with mocked driver."""
|
||||
mock_driver = mocker.Mock()
|
||||
return LinkedInJobManager(mock_driver)
|
||||
|
||||
|
||||
def test_initialization(job_manager):
|
||||
"""Test LinkedInJobManager initialization."""
|
||||
assert job_manager.driver is not None
|
||||
assert job_manager.set_old_answers == set()
|
||||
assert job_manager.easy_applier_component is None
|
||||
|
||||
|
||||
def test_set_parameters(mocker, job_manager):
|
||||
"""Test setting parameters for the LinkedInJobManager."""
|
||||
# Mocking os.path.exists to return True for the resume path
|
||||
mocker.patch('pathlib.Path.exists', return_value=True)
|
||||
|
||||
params = {
|
||||
'company_blacklist': ['Company A', 'Company B'],
|
||||
'title_blacklist': ['Intern', 'Junior'],
|
||||
'positions': ['Software Engineer', 'Data Scientist'],
|
||||
'locations': ['New York', 'San Francisco'],
|
||||
'apply_once_at_company': True,
|
||||
'uploads': {'resume': '/path/to/resume'}, # Resume path provided here
|
||||
'outputFileDirectory': '/path/to/output',
|
||||
'job_applicants_threshold': {
|
||||
'min_applicants': 5,
|
||||
'max_applicants': 50
|
||||
},
|
||||
'remote': False,
|
||||
'distance': 50,
|
||||
'date': {'all time': True}
|
||||
}
|
||||
|
||||
job_manager.set_parameters(params)
|
||||
|
||||
# Normalize paths to handle platform differences (e.g., Windows vs Unix-like systems)
|
||||
assert str(job_manager.resume_path) == os.path.normpath('/path/to/resume')
|
||||
assert str(job_manager.output_file_directory) == os.path.normpath(
|
||||
'/path/to/output')
|
||||
|
||||
|
||||
def next_job_page(self, position, location, job_page):
|
||||
logger.debug("Navigating to next job page: %s in %s, page %d",
|
||||
position, location, job_page)
|
||||
self.driver.get(
|
||||
f"https://www.linkedin.com/jobs/search/{self.base_search_url}&keywords={position}&location={location}&start={job_page * 25}")
|
||||
|
||||
|
||||
def test_get_jobs_from_page_no_jobs(mocker, job_manager):
|
||||
"""Test get_jobs_from_page when no jobs are found."""
|
||||
mocker.patch.object(job_manager.driver, 'find_element',
|
||||
side_effect=NoSuchElementException)
|
||||
|
||||
jobs = job_manager.get_jobs_from_page()
|
||||
assert jobs == []
|
||||
|
||||
|
||||
def test_get_jobs_from_page_with_jobs(mocker, job_manager):
|
||||
"""Test get_jobs_from_page when job elements are found."""
|
||||
# Mock the no_jobs_element to behave correctly
|
||||
mock_no_jobs_element = mocker.Mock()
|
||||
mock_no_jobs_element.text = "No matching jobs found"
|
||||
|
||||
# Mocking the find_element to return the mock no_jobs_element
|
||||
mocker.patch.object(job_manager.driver, 'find_element',
|
||||
return_value=mock_no_jobs_element)
|
||||
|
||||
# Mock the page_source
|
||||
mocker.patch.object(job_manager.driver, 'page_source',
|
||||
return_value="some page content")
|
||||
|
||||
# Ensure jobs are returned as empty list due to "No matching jobs found"
|
||||
jobs = job_manager.get_jobs_from_page()
|
||||
assert jobs == [] # No jobs expected due to "No matching jobs found"
|
||||
|
||||
|
||||
def test_apply_jobs_with_no_jobs(mocker, job_manager):
|
||||
"""Test apply_jobs when no jobs are found."""
|
||||
# Mocking find_element to return a mock element that simulates no jobs
|
||||
mock_element = mocker.Mock()
|
||||
mock_element.text = "No matching jobs found"
|
||||
|
||||
# Mock the driver to simulate the page source
|
||||
mocker.patch.object(job_manager.driver, 'page_source', return_value="")
|
||||
|
||||
# Mock the driver to return the mock element when find_element is called
|
||||
mocker.patch.object(job_manager.driver, 'find_element',
|
||||
return_value=mock_element)
|
||||
|
||||
# Call apply_jobs and ensure no exceptions are raised
|
||||
job_manager.apply_jobs()
|
||||
|
||||
# Ensure it attempted to find the job results list
|
||||
assert job_manager.driver.find_element.call_count == 1
|
||||
|
||||
|
||||
def test_apply_jobs_with_jobs(mocker, job_manager):
|
||||
"""Test apply_jobs when jobs are present."""
|
||||
|
||||
# Mock no_jobs_element to simulate the absence of "No matching jobs found" banner
|
||||
no_jobs_element = mocker.Mock()
|
||||
no_jobs_element.text = "" # Empty text means "No matching jobs found" is not present
|
||||
mocker.patch.object(job_manager.driver, 'find_element',
|
||||
return_value=no_jobs_element)
|
||||
|
||||
# Mock the page_source to simulate what the page looks like when jobs are present
|
||||
mocker.patch.object(job_manager.driver, 'page_source',
|
||||
return_value="some job content")
|
||||
|
||||
# Mock the outer find_elements (scaffold-layout__list-container)
|
||||
container_mock = mocker.Mock()
|
||||
|
||||
# Mock the inner find_elements to return job list items
|
||||
job_element_mock = mocker.Mock()
|
||||
# Simulating two job items
|
||||
job_elements_list = [job_element_mock, job_element_mock]
|
||||
|
||||
# Return the container mock, which itself returns the job elements list
|
||||
container_mock.find_elements.return_value = job_elements_list
|
||||
mocker.patch.object(job_manager.driver, 'find_elements',
|
||||
return_value=[container_mock])
|
||||
|
||||
# Mock the extract_job_information_from_tile method to return sample job info
|
||||
mocker.patch.object(job_manager, 'extract_job_information_from_tile', return_value=(
|
||||
"Title", "Company", "Location", "Apply", "Link"))
|
||||
|
||||
# Mock other methods like is_blacklisted, is_already_applied_to_job, and is_already_applied_to_company
|
||||
mocker.patch.object(job_manager, 'is_blacklisted', return_value=False)
|
||||
mocker.patch.object(
|
||||
job_manager, 'is_already_applied_to_job', return_value=False)
|
||||
mocker.patch.object(
|
||||
job_manager, 'is_already_applied_to_company', return_value=False)
|
||||
|
||||
# Mock the LinkedInEasyApplier component
|
||||
job_manager.easy_applier_component = mocker.Mock()
|
||||
|
||||
# Mock the output_file_directory as a valid Path object
|
||||
job_manager.output_file_directory = Path("/mocked/path/to/output")
|
||||
|
||||
# Mock Path.exists() to always return True (so no actual file system interaction is needed)
|
||||
mocker.patch.object(Path, 'exists', return_value=True)
|
||||
|
||||
# Mock the open function to prevent actual file writing
|
||||
mock_open = mocker.mock_open()
|
||||
mocker.patch('builtins.open', mock_open)
|
||||
|
||||
# Run the apply_jobs method
|
||||
job_manager.apply_jobs()
|
||||
|
||||
# Assertions
|
||||
assert job_manager.driver.find_elements.call_count == 1
|
||||
# Called for each job element
|
||||
assert job_manager.extract_job_information_from_tile.call_count == 2
|
||||
# Called for each job element
|
||||
assert job_manager.easy_applier_component.job_apply.call_count == 2
|
||||
mock_open.assert_called() # Ensure that the open function was called
|
||||
96
tests/test_utils.py
Normal file
96
tests/test_utils.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# tests/test_utils.py
|
||||
import pytest
|
||||
import os
|
||||
import time
|
||||
from unittest import mock
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
from src.utils import ensure_chrome_profile, is_scrollable, scroll_slow, chrome_browser_options, printred, printyellow
|
||||
|
||||
# Mocking logging to avoid actual file writing
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_logger(mocker):
|
||||
mocker.patch("src.utils.logger")
|
||||
|
||||
# Test ensure_chrome_profile function
|
||||
def test_ensure_chrome_profile(mocker):
|
||||
mocker.patch("os.path.exists", return_value=False) # Pretend directory doesn't exist
|
||||
mocker.patch("os.makedirs") # Mock making directories
|
||||
|
||||
# Call the function
|
||||
profile_path = ensure_chrome_profile()
|
||||
|
||||
# Verify that os.makedirs was called twice to create the directory
|
||||
assert profile_path.endswith("linkedin_profile")
|
||||
assert os.path.exists.called
|
||||
assert os.makedirs.called
|
||||
|
||||
# Test is_scrollable function
|
||||
def test_is_scrollable(mocker):
|
||||
mock_element = mocker.Mock(spec=WebElement)
|
||||
mock_element.get_attribute.side_effect = lambda attr: "1000" if attr == "scrollHeight" else "500"
|
||||
|
||||
# Call the function
|
||||
scrollable = is_scrollable(mock_element)
|
||||
|
||||
# Check the expected outcome
|
||||
assert scrollable is True
|
||||
mock_element.get_attribute.assert_any_call("scrollHeight")
|
||||
mock_element.get_attribute.assert_any_call("clientHeight")
|
||||
|
||||
# Test scroll_slow function
|
||||
def test_scroll_slow(mocker):
|
||||
mock_driver = mocker.Mock()
|
||||
mock_element = mocker.Mock(spec=WebElement)
|
||||
|
||||
# Mock element's attributes for scrolling
|
||||
mock_element.get_attribute.side_effect = lambda attr: "2000" if attr == "scrollHeight" else "0"
|
||||
mock_element.is_displayed.return_value = True
|
||||
mocker.patch("time.sleep") # Mock time.sleep to avoid waiting
|
||||
|
||||
# Call the function
|
||||
scroll_slow(mock_driver, mock_element, start=0, end=1000, step=100, reverse=False)
|
||||
|
||||
# Ensure that scrolling happened multiple times
|
||||
assert mock_driver.execute_script.called
|
||||
mock_element.is_displayed.assert_called_once()
|
||||
|
||||
def test_scroll_slow_element_not_scrollable(mocker):
|
||||
mock_driver = mocker.Mock()
|
||||
mock_element = mocker.Mock(spec=WebElement)
|
||||
|
||||
# Mock the attributes so the element is not scrollable
|
||||
mock_element.get_attribute.side_effect = lambda attr: "1000" if attr == "scrollHeight" else "1000"
|
||||
mock_element.is_displayed.return_value = True
|
||||
|
||||
scroll_slow(mock_driver, mock_element, start=0, end=1000, step=100)
|
||||
|
||||
# Ensure it detected non-scrollable element
|
||||
mock_driver.execute_script.assert_not_called()
|
||||
|
||||
# Test chrome_browser_options function
|
||||
def test_chrome_browser_options(mocker):
|
||||
mocker.patch("src.utils.ensure_chrome_profile")
|
||||
mocker.patch("os.path.dirname", return_value="/mocked/path")
|
||||
mocker.patch("os.path.basename", return_value="profile_directory")
|
||||
|
||||
mock_options = mocker.Mock()
|
||||
|
||||
mocker.patch("selenium.webdriver.ChromeOptions", return_value=mock_options)
|
||||
|
||||
# Call the function
|
||||
options = chrome_browser_options()
|
||||
|
||||
# Ensure options were set
|
||||
assert mock_options.add_argument.called
|
||||
assert options == mock_options
|
||||
|
||||
# Test printred and printyellow functions
|
||||
def test_printred(mocker):
|
||||
mocker.patch("builtins.print")
|
||||
printred("Test")
|
||||
print.assert_called_once_with("\033[91mTest\033[0m")
|
||||
|
||||
def test_printyellow(mocker):
|
||||
mocker.patch("builtins.print")
|
||||
printyellow("Test")
|
||||
print.assert_called_once_with("\033[93mTest\033[0m")
|
||||
Loading…
Add table
Add a link
Reference in a new issue