diff --git a/src/dataset/dump_data.py b/src/dataset/dump_data.py index 40cc96a..31b0423 100755 --- a/src/dataset/dump_data.py +++ b/src/dataset/dump_data.py @@ -1,25 +1,26 @@ #!/usr/bin/env python3 -import subprocess +import subprocess # noqa: S404 from pathlib import Path + from template_project.web_api.configuration import load_configuration def main() -> None: config_path = Path("config.toml") configuration = load_configuration(config_path) - + db_url = str(configuration.database.url.get_value()) db_url = db_url.replace("postgresql+psycopg://", "postgresql://") - + output_dir = Path("dumps") output_dir.mkdir(exist_ok=True) - + output_file = output_dir / "data_dump.sql" - + print("Создание дампа таблиц vacancy, vacancy_embedding, key_skills...") - - subprocess.run( - [ + + subprocess.run( # noqa: S603 + [ # noqa: S607 "pg_dump", db_url, "--table=vacancy", @@ -31,7 +32,7 @@ def main() -> None: ], check=True, ) - + print(f"\nДамп создан: {output_file}") print(f"Размер файла: {output_file.stat().st_size / 1024 / 1024:.2f} MB") print("\nДля импорта на прод сервере выполните:") @@ -40,4 +41,3 @@ def main() -> None: if __name__ == "__main__": main() - diff --git a/src/template_project/adapters/data_gateways/tables.py b/src/template_project/adapters/data_gateways/tables.py index 9e654f9..5adf4de 100644 --- a/src/template_project/adapters/data_gateways/tables.py +++ b/src/template_project/adapters/data_gateways/tables.py @@ -93,8 +93,8 @@ class ExperienceTypeType(TypeDecorator[ExperienceType]): if isinstance(value, str): try: return ExperienceType(value) - except ValueError: - raise ValueError(f"Invalid experience_type value: {value}") + except ValueError as err: + raise ValueError(f"Invalid experience_type value: {value}") from err raise ValueError(f"Cannot convert {type(value)} to ExperienceType") diff --git a/tests/unit/test_access_token_entity.py b/tests/unit/test_access_token_entity.py new file mode 100644 index 0000000..d2f361a --- /dev/null +++ b/tests/unit/test_access_token_entity.py @@ -0,0 +1,93 @@ +from datetime import UTC, datetime, timedelta +from uuid import UUID, uuid4 + +import pytest + +from template_project.application.access_token.entity import AccessToken +from template_project.application.access_token.errors import AccessTokenExpiredError +from template_project.application.user.entity import UserId + + +def test_access_token_factory_creates_valid_token() -> None: + user_id = UserId(uuid4()) + expires_in = timedelta(days=7) + + token = AccessToken.factory( + user_id=user_id, + expires_in=expires_in, + ) + + assert isinstance(token.id, UUID) + assert token.user_id == user_id + assert token.revoked is False + assert token.expires_in > datetime.now(tz=UTC) + assert token.deleted_at is None + assert token.created_at.tzinfo == UTC + + +def test_access_token_expired_predicate_not_expired() -> None: + user_id = UserId(uuid4()) + token = AccessToken.factory( + user_id=user_id, + expires_in=timedelta(days=7), + ) + + assert not token.expired_predicate() + + +def test_access_token_expired_predicate_expired_by_time() -> None: + user_id = UserId(uuid4()) + token = AccessToken.factory( + user_id=user_id, + expires_in=timedelta(days=-1), + ) + + assert token.expired_predicate() + + +def test_access_token_expired_predicate_revoked() -> None: + user_id = UserId(uuid4()) + token = AccessToken.factory( + user_id=user_id, + expires_in=timedelta(days=7), + ) + token.revoke() + + assert token.expired_predicate() + + +def test_access_token_ensure_expired_raises_when_expired() -> None: + user_id = UserId(uuid4()) + token = AccessToken.factory( + user_id=user_id, + expires_in=timedelta(days=-1), + ) + + with pytest.raises(AccessTokenExpiredError): + token.ensure_expired() + + +def test_access_token_ensure_expired_raises_when_revoked() -> None: + user_id = UserId(uuid4()) + token = AccessToken.factory( + user_id=user_id, + expires_in=timedelta(days=7), + ) + token.revoke() + + with pytest.raises(AccessTokenExpiredError): + token.ensure_expired() + + +def test_access_token_revoke() -> None: + user_id = UserId(uuid4()) + token = AccessToken.factory( + user_id=user_id, + expires_in=timedelta(days=7), + ) + + token.revoke() + + assert token.revoked is True + assert token.deleted_at is not None + assert token.deleted_at.tzinfo == UTC diff --git a/tests/unit/test_auth_identity_entity.py b/tests/unit/test_auth_identity_entity.py new file mode 100644 index 0000000..68f14cc --- /dev/null +++ b/tests/unit/test_auth_identity_entity.py @@ -0,0 +1,36 @@ +from datetime import UTC +from uuid import UUID, uuid4 + +from template_project.application.auth_identity.entity import AuthIdentity, AuthMethod +from template_project.application.user.entity import UserId + + +def test_auth_identity_factory_creates_valid_identity() -> None: + user_id = UserId(uuid4()) + + identity = AuthIdentity.factory( + user_id=user_id, + method=AuthMethod.EMAIL, + identifier="test@example.com", + secret_key="hashed_password", + ) + + assert isinstance(identity.id, UUID) + assert identity.user_id == user_id + assert identity.method == AuthMethod.EMAIL + assert identity.identifier == "test@example.com" + assert identity.secret_key == "hashed_password" # noqa: S105 + assert identity.deleted_at is None + assert identity.created_at.tzinfo == UTC + + +def test_auth_identity_factory_without_secret_key() -> None: + user_id = UserId(uuid4()) + + identity = AuthIdentity.factory( + user_id=user_id, + method=AuthMethod.YANDEX, + identifier="yandex_id_123", + ) + + assert identity.secret_key is None diff --git a/tests/unit/test_notification_device_entity.py b/tests/unit/test_notification_device_entity.py new file mode 100644 index 0000000..f8108d1 --- /dev/null +++ b/tests/unit/test_notification_device_entity.py @@ -0,0 +1,20 @@ +from datetime import UTC +from uuid import UUID, uuid4 + +from template_project.application.notification_device.entity import NotificationDevice +from template_project.application.user.entity import UserId + + +def test_notification_device_factory_creates_valid_device() -> None: + user_id = UserId(uuid4()) + + device = NotificationDevice.factory( + user_id=user_id, + device_id="device_123", + ) + + assert isinstance(device.id, UUID) + assert device.user_id == user_id + assert device.device_id == "device_123" + assert device.deleted_at is None + assert device.created_at.tzinfo == UTC diff --git a/tests/unit/test_profile_entity.py b/tests/unit/test_profile_entity.py new file mode 100644 index 0000000..3c58c5f --- /dev/null +++ b/tests/unit/test_profile_entity.py @@ -0,0 +1,44 @@ +from datetime import UTC +from uuid import UUID, uuid4 + +from template_project.application.user.entity import UserId +from template_project.application.user.profile.entity import Profile + + +def test_profile_factory_creates_valid_profile() -> None: + user_id = UserId(uuid4()) + + profile = Profile.factory( + user_id=user_id, + email="test@example.com", + display_name="Test User", + first_name="Test", + last_name="User", + avatar_url="https://example.com/avatar.jpg", + phone="+1234567890", + ) + + assert isinstance(profile.id, UUID) + assert profile.user_id == user_id + assert profile.email == "test@example.com" + assert profile.display_name == "Test User" + assert profile.first_name == "Test" + assert profile.last_name == "User" + assert profile.avatar_url == "https://example.com/avatar.jpg" + assert profile.phone == "+1234567890" + assert profile.deleted_at is None + assert profile.created_at.tzinfo == UTC + + +def test_profile_factory_with_minimal_fields() -> None: + user_id = UserId(uuid4()) + + profile = Profile.factory(user_id=user_id) + + assert profile.user_id == user_id + assert profile.email is None + assert profile.display_name is None + assert profile.first_name is None + assert profile.last_name is None + assert profile.avatar_url is None + assert profile.phone is None diff --git a/tests/unit/test_resume_entity.py b/tests/unit/test_resume_entity.py new file mode 100644 index 0000000..6b67bbc --- /dev/null +++ b/tests/unit/test_resume_entity.py @@ -0,0 +1,229 @@ +from datetime import UTC +from decimal import Decimal +from uuid import UUID, uuid4 + +from template_project.application.common.enums import EducationGrade, ExperienceType +from template_project.application.resume.entity import ( + Resume, + ResumeEducation, + ResumeEmbedding, + ResumeExperience, + ResumeId, + ResumePrediction, + ResumeProject, +) +from template_project.application.user.entity import UserId + + +def test_resume_factory_creates_valid_resume() -> None: + user_id = UserId(uuid4()) + resume = Resume.factory( + user_id=user_id, + position="Python Developer", + location="Moscow", + about_me="Experienced developer", + key_skills=["Python", "FastAPI"], + experience_type=ExperienceType.BETWEEN_3_AND_6, + ) + + assert isinstance(resume.id, UUID) + assert resume.user_id == user_id + assert resume.position == "Python Developer" + assert resume.location == "Moscow" + assert resume.about_me == "Experienced developer" + assert resume.key_skills == ["Python", "FastAPI"] + assert resume.experience_type == ExperienceType.BETWEEN_3_AND_6 + assert resume.down_resume_id is None + assert resume.up_resume_id is None + assert resume.deleted_at is None + assert resume.created_at.tzinfo == UTC + + +def test_resume_factory_with_history_links() -> None: + user_id = UserId(uuid4()) + old_resume_id = ResumeId(uuid4()) + + resume = Resume.factory( + user_id=user_id, + position="Python Developer", + location="Moscow", + about_me="Updated", + key_skills=["Python"], + experience_type=ExperienceType.BETWEEN_3_AND_6, + down_resume_id=old_resume_id, + ) + + assert resume.down_resume_id == old_resume_id + + +def test_resume_embedding_factory_creates_valid_embedding() -> None: + resume_id = ResumeId(uuid4()) + vector = [0.1, 0.2, 0.3, 0.4, 0.5] + + embedding = ResumeEmbedding.factory( + resume_id=resume_id, + vector=vector, + ) + + assert isinstance(embedding.id, UUID) + assert embedding.resume_id == resume_id + assert embedding.vector == vector + assert embedding.deleted_at is None + assert embedding.created_at.tzinfo == UTC + + +def test_resume_embedding_vector_dimension() -> None: + resume_id = ResumeId(uuid4()) + vector = [0.1] * 384 + + embedding = ResumeEmbedding.factory( + resume_id=resume_id, + vector=vector, + ) + + assert len(embedding.vector) == 384 + assert all(isinstance(x, float) for x in embedding.vector) + + +def test_resume_prediction_factory_creates_valid_prediction() -> None: + resume_id = ResumeId(uuid4()) + + prediction = ResumePrediction.factory( + resume_id=resume_id, + from_salary=Decimal(100000), + to_salary=Decimal(150000), + recommended_skills=["Kubernetes", "Docker"], + ) + + assert isinstance(prediction.id, UUID) + assert prediction.resume_id == resume_id + assert prediction.from_salary == Decimal(100000) + assert prediction.to_salary == Decimal(150000) + assert prediction.recommended_skills == ["Kubernetes", "Docker"] + assert prediction.deleted_at is None + assert prediction.created_at.tzinfo == UTC + + +def test_resume_prediction_salary_order() -> None: + resume_id = ResumeId(uuid4()) + + prediction = ResumePrediction.factory( + resume_id=resume_id, + from_salary=Decimal(100000), + to_salary=Decimal(150000), + recommended_skills=[], + ) + + assert prediction.from_salary <= prediction.to_salary + + +def test_resume_prediction_empty_recommended_skills() -> None: + resume_id = ResumeId(uuid4()) + + prediction = ResumePrediction.factory( + resume_id=resume_id, + from_salary=Decimal(100000), + to_salary=Decimal(150000), + recommended_skills=[], + ) + + assert prediction.recommended_skills == [] + + +def test_resume_experience_positive_duration() -> None: + resume_id = ResumeId(uuid4()) + + experience = ResumeExperience.factory( + resume_id=resume_id, + place="Company", + description="Work", + months_duration=12, + ) + + assert experience.months_duration > 0 + + +def test_resume_key_skills_empty_list() -> None: + user_id = UserId(uuid4()) + + resume = Resume.factory( + user_id=user_id, + position="Developer", + location="Moscow", + about_me="Test", + key_skills=[], + experience_type=ExperienceType.NO_EXPERIENCE, + ) + + assert resume.key_skills == [] + + +def test_resume_experience_factory_creates_valid_experience() -> None: + resume_id = ResumeId(uuid4()) + + experience = ResumeExperience.factory( + resume_id=resume_id, + place="T-bank", + description="Backend development", + months_duration=24, + ) + + assert isinstance(experience.id, UUID) + assert experience.resume_id == resume_id + assert experience.place == "T-bank" + assert experience.description == "Backend development" + assert experience.months_duration == 24 + assert experience.months_duration > 0 + assert experience.deleted_at is None + assert experience.created_at.tzinfo == UTC + + +def test_resume_education_factory_creates_valid_education() -> None: + resume_id = ResumeId(uuid4()) + + education = ResumeEducation.factory( + resume_id=resume_id, + place="University", + grade=EducationGrade.BACHELOR, + specialization="Computer Science", + description="Optional description", + ) + + assert isinstance(education.id, UUID) + assert education.resume_id == resume_id + assert education.place == "University" + assert education.grade == EducationGrade.BACHELOR + assert education.specialization == "Computer Science" + assert education.description == "Optional description" + assert education.deleted_at is None + assert education.created_at.tzinfo == UTC + + +def test_resume_education_factory_without_description() -> None: + resume_id = ResumeId(uuid4()) + + education = ResumeEducation.factory( + resume_id=resume_id, + place="University", + grade=EducationGrade.MASTER, + specialization="Data Science", + ) + + assert education.description is None + + +def test_resume_project_factory_creates_valid_project() -> None: + resume_id = ResumeId(uuid4()) + + project = ResumeProject.factory( + resume_id=resume_id, + name="ML Service", + description="Machine learning service", + ) + + assert isinstance(project.id, UUID) + assert project.resume_id == resume_id + assert project.name == "ML Service" + assert project.description == "Machine learning service" + assert project.deleted_at is None + assert project.created_at.tzinfo == UTC diff --git a/tests/unit/test_user_entity.py b/tests/unit/test_user_entity.py new file mode 100644 index 0000000..aef88a9 --- /dev/null +++ b/tests/unit/test_user_entity.py @@ -0,0 +1,12 @@ +from datetime import UTC +from uuid import UUID + +from template_project.application.user.entity import User + + +def test_user_factory_creates_valid_user() -> None: + user = User.factory() + + assert isinstance(user.id, UUID) + assert user.deleted_at is None + assert user.created_at.tzinfo == UTC diff --git a/tests/unit/test_vacancy_entity.py b/tests/unit/test_vacancy_entity.py new file mode 100644 index 0000000..3421107 --- /dev/null +++ b/tests/unit/test_vacancy_entity.py @@ -0,0 +1,95 @@ +from datetime import UTC +from decimal import Decimal +from uuid import UUID, uuid4 + +from template_project.application.common.enums import ExperienceType +from template_project.application.vacancy.entity import Vacancy, VacancyEmbedding, VacancyId + + +def test_vacancy_factory_creates_valid_vacancy() -> None: + vacancy = Vacancy.factory( + position="Python Developer", + from_salary=Decimal(100000), + to_salary=Decimal(150000), + experience_type=ExperienceType.BETWEEN_3_AND_6, + description="Backend development", + key_skills=["Python", "FastAPI"], + ) + + assert isinstance(vacancy.id, UUID) + assert vacancy.position == "Python Developer" + assert vacancy.from_salary == Decimal(100000) + assert vacancy.to_salary == Decimal(150000) + assert vacancy.experience_type == ExperienceType.BETWEEN_3_AND_6 + assert vacancy.description == "Backend development" + assert vacancy.key_skills == ["Python", "FastAPI"] + assert vacancy.deleted_at is None + assert vacancy.created_at.tzinfo == UTC + + +def test_vacancy_salary_order() -> None: + vacancy = Vacancy.factory( + position="Developer", + from_salary=Decimal(100000), + to_salary=Decimal(150000), + experience_type=ExperienceType.BETWEEN_1_AND_3, + description="Test", + key_skills=[], + ) + + assert vacancy.from_salary <= vacancy.to_salary + + +def test_vacancy_equal_salaries() -> None: + vacancy = Vacancy.factory( + position="Developer", + from_salary=Decimal(100000), + to_salary=Decimal(100000), + experience_type=ExperienceType.BETWEEN_1_AND_3, + description="Test", + key_skills=[], + ) + + assert vacancy.from_salary == vacancy.to_salary + + +def test_vacancy_empty_key_skills() -> None: + vacancy = Vacancy.factory( + position="Developer", + from_salary=Decimal(100000), + to_salary=Decimal(150000), + experience_type=ExperienceType.NO_EXPERIENCE, + description="Test", + key_skills=[], + ) + + assert vacancy.key_skills == [] + + +def test_vacancy_embedding_vector_dimension() -> None: + vacancy_id = VacancyId(uuid4()) + vector = [0.1] * 384 + + embedding = VacancyEmbedding.factory( + vacancy_id=vacancy_id, + vector=vector, + ) + + assert len(embedding.vector) == 384 + assert all(isinstance(x, float) for x in embedding.vector) + + +def test_vacancy_embedding_factory_creates_valid_embedding() -> None: + vacancy_id = VacancyId(uuid4()) + vector = [0.1, 0.2, 0.3, 0.4, 0.5] + + embedding = VacancyEmbedding.factory( + vacancy_id=vacancy_id, + vector=vector, + ) + + assert isinstance(embedding.id, UUID) + assert embedding.vacancy_id == vacancy_id + assert embedding.vector == vector + assert embedding.deleted_at is None + assert embedding.created_at.tzinfo == UTC