diff --git a/app/controllers/quiz_controller.py b/app/controllers/quiz_controller.py index 5fd58b8..27f18e6 100644 --- a/app/controllers/quiz_controller.py +++ b/app/controllers/quiz_controller.py @@ -42,7 +42,7 @@ class QuizController: result = self.quiz_service.get_quiz_recommendation() if not result: return make_response(message="Quiz not found", status_code=404) - return make_response(message="Quiz Found", data=result.dict()) + return make_response(message="Quiz Found", data=result.model_dump()) except Exception as e: return make_error_response(e) diff --git a/app/mapper/__init__.py b/app/mapper/__init__.py index dcfac5b..a125e5d 100644 --- a/app/mapper/__init__.py +++ b/app/mapper/__init__.py @@ -1,8 +1,10 @@ from .user_mapper import UserMapper from .quiz_mapper import QuizMapper +from .subject_mapper import SubjectMapper __all__ = [ "UserMapper", "QuizMapper", + "SubjectMapper", ] diff --git a/app/mapper/subject_mapper.py b/app/mapper/subject_mapper.py new file mode 100644 index 0000000..96ceded --- /dev/null +++ b/app/mapper/subject_mapper.py @@ -0,0 +1,12 @@ +from app.schemas.requests import SubjectCreateRequest +from app.models.entities import SubjectEntity + + +class SubjectMapper: + @staticmethod + def to_entity(data: SubjectCreateRequest) -> SubjectEntity: + return SubjectEntity( + name=data.name, + short_name=data.alias, + description=data.description, + ) diff --git a/app/models/entities/quiz_entity.py b/app/models/entities/quiz_entity.py index c53cc86..96f1d2e 100644 --- a/app/models/entities/quiz_entity.py +++ b/app/models/entities/quiz_entity.py @@ -18,7 +18,7 @@ class QuizEntity(BaseModel): total_user_playing: int = 0 question_listings: Optional[list[QuestionItemEntity]] = [] - class Config: + class ConfigDict: arbitrary_types_allowed = True populate_by_name = True json_encoders = {PyObjectId: str} diff --git a/app/models/entities/subject_entity.py b/app/models/entities/subject_entity.py index 3d6b8bd..a401de5 100644 --- a/app/models/entities/subject_entity.py +++ b/app/models/entities/subject_entity.py @@ -11,14 +11,7 @@ class SubjectEntity(BaseModel): description: Optional[str] = None icon: Optional[str] = None - class Config: + class ConfigDict: populate_by_name = True json_encoders = {ObjectId: str} - json_schema_extra = { - "example": { - "_id": "sejarah", - "name": "Sejarah", - "description": "Kuis tentang sejarah Indonesia", - "icon": "http://", - } - } + json_schema_extra = {} diff --git a/app/models/entities/user_answer_entity.py b/app/models/entities/user_answer_entity.py index 23b1501..f54ae5c 100644 --- a/app/models/entities/user_answer_entity.py +++ b/app/models/entities/user_answer_entity.py @@ -17,7 +17,7 @@ class UserAnswerEntity(BaseModel): total_score: int total_correct: int - class Config: + class ConfigDict: populate_by_name = True arbitrary_types_allowed = True json_encoders = {ObjectId: str} diff --git a/app/models/entities/user_entity.py b/app/models/entities/user_entity.py index 96a65b0..204ce04 100644 --- a/app/models/entities/user_entity.py +++ b/app/models/entities/user_entity.py @@ -1,5 +1,5 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, ConfigDict from datetime import datetime from .base import PyObjectId @@ -17,6 +17,4 @@ class UserEntity(BaseModel): created_at: Optional[datetime] = None updated_at: Optional[datetime] = None - class Config: - populate_by_name = True - json_encoders = {PyObjectId: str} + model_config = ConfigDict(populate_by_name=True, json_encoders={PyObjectId: str}) diff --git a/app/models/login/login_response.py b/app/models/login/login_response.py index dff46ee..3b455db 100644 --- a/app/models/login/login_response.py +++ b/app/models/login/login_response.py @@ -13,7 +13,7 @@ class UserResponseModel(BaseModel): phone: Optional[str] = None locale: str - class Config: + class ConfigDict: populate_by_name = True json_encoders = { datetime: lambda v: v.isoformat(), diff --git a/app/schemas/response/subject/get_subject_schema.py b/app/schemas/response/subject/get_subject_schema.py index b08ba72..1344971 100644 --- a/app/schemas/response/subject/get_subject_schema.py +++ b/app/schemas/response/subject/get_subject_schema.py @@ -8,6 +8,6 @@ class GetSubjectResponse(BaseModel): alias: str description: Optional[str] - class Config: + class ConfigDict: from_attributes = True populate_by_name = True diff --git a/app/services/answer_service.py b/app/services/answer_service.py index 559b915..17a912a 100644 --- a/app/services/answer_service.py +++ b/app/services/answer_service.py @@ -55,18 +55,15 @@ class AnswerService: elif question.type == "true_false": correct = user_answer.answer == question.target_answer elif question.type == "option": - try: - answer_index = int(user_answer.answer) - if 0 <= answer_index < len(question.options): - correct = str(answer_index) == question.target_answer - else: - raise ValueError( - f"Index jawaban tidak valid untuk soal {question.index}" - ) - except ValueError: + + answer_index = int(user_answer.answer) + if 0 <= answer_index < len(question.options): + correct = str(answer_index) == question.target_answer + else: raise ValueError( - f"Jawaban bukan index valid untuk soal {question.index}" + f"Index jawaban tidak valid untuk soal {question.index}" ) + else: raise ValueError(f"Tipe soal tidak dikenali: {question.type}") diff --git a/app/services/auth_service.py b/app/services/auth_service.py index 57b6ba3..f32ebe0 100644 --- a/app/services/auth_service.py +++ b/app/services/auth_service.py @@ -17,9 +17,6 @@ class AuthService: id_token_str, requests.Request(), Config.GOOGLE_CLIENT_ID ) - if not payload: - raise AuthException("Invalid Google ID Token") - google_id = payload.get("sub") email = payload.get("email") diff --git a/app/services/history_service.py b/app/services/history_service.py index 0c6ab3d..81a9b1e 100644 --- a/app/services/history_service.py +++ b/app/services/history_service.py @@ -1,5 +1,9 @@ from app.repositories import UserAnswerRepository, QuizRepository -from app.schemas.response import HistoryResultSchema, QuizHistoryResponse, QuestionResult +from app.schemas.response import ( + HistoryResultSchema, + QuizHistoryResponse, + QuestionResult, +) class HistoryService: @@ -19,7 +23,7 @@ class HistoryService: quiz_ids = [asn.quiz_id for asn in answer_data] quiz_data = self.quiz_repository.get_by_ids(quiz_ids) quiz_map = {str(quiz.id): quiz for quiz in quiz_data} - + print(quiz_map) result = [] for answer in answer_data: quiz = quiz_map.get(answer.quiz_id) diff --git a/app/services/quiz_service.py b/app/services/quiz_service.py index bb43f73..bef5256 100644 --- a/app/services/quiz_service.py +++ b/app/services/quiz_service.py @@ -46,7 +46,7 @@ class QuizService: if author is None: continue mapped_quizzes.append( - QuizMapper.quiz_to_recomendation_app.mapper( + QuizMapper.quiz_to_recomendation_mapper( quiz_entity=quiz, user_entity=author, ) @@ -68,7 +68,7 @@ class QuizService: user = self.user_repostory.get_user_by_id(user_id) quiz_data = [ - QuizMapper.quiz_to_recomendation_app.mapper(quiz, user) for quiz in quizzes + QuizMapper.quiz_to_recomendation_mapper(quiz, user) for quiz in quizzes ] return UserQuizListResponse(total=total_user_quiz, quizzes=quiz_data) @@ -110,7 +110,7 @@ class QuizService: for quiz in data: author = self.user_repostory.get_user_by_id(user_id=quiz.author_id) result.append( - QuizMapper.quiz_to_recomendation_app.mapper( + QuizMapper.quiz_to_recomendation_mapper( quiz_entity=quiz, user_entity=author, ) diff --git a/app/services/subject_service.py b/app/services/subject_service.py index e4644d7..e7364da 100644 --- a/app/services/subject_service.py +++ b/app/services/subject_service.py @@ -3,6 +3,7 @@ from app.models.entities import SubjectEntity from app.schemas.requests import SubjectCreateRequest, SubjectUpdateRequest from app.schemas.response import GetSubjectResponse from app.repositories import SubjectRepository +from app.mapper import SubjectMapper class SubjectService: @@ -10,7 +11,7 @@ class SubjectService: self.repository = repository def create_subject(self, request: SubjectCreateRequest) -> str: - subject = SubjectEntity(**request) + subject = SubjectMapper.to_entity(request) return self.repository.create(subject) def get_all_subjects(self) -> List[GetSubjectResponse]: @@ -37,7 +38,7 @@ class SubjectService: return None def update_subject(self, subject_id: str, request: SubjectUpdateRequest) -> bool: - update_data = request.dict(exclude_unset=True) + update_data = request.model_dump(exclude_unset=True) return self.repository.update(subject_id, update_data) def delete_subject(self, subject_id: str) -> bool: diff --git a/pytest.ini b/pytest.ini index b8757d1..5ab54f2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,7 @@ # pytest.ini [pytest] pythonpath = . + +filterwarnings = + ignore::DeprecationWarning + ignore::UserWarning diff --git a/run.py b/run.py index 33ad077..a4bb1cf 100644 --- a/run.py +++ b/run.py @@ -3,6 +3,5 @@ from app.main import createApp, socketio app = createApp() -# if __name__ == "__main__": -# # Untuk dev/testing -# socketio.run(app, host="0.0.0.0", port=5000, debug=True) +if __name__ == "__main__": + socketio.run(app, host="0.0.0.0", port=5000, debug=True) diff --git a/test/service/__pycache__/test_answer_service.cpython-310-pytest-8.3.4.pyc b/test/service/__pycache__/test_answer_service.cpython-310-pytest-8.3.4.pyc new file mode 100644 index 0000000..0c58671 Binary files /dev/null and b/test/service/__pycache__/test_answer_service.cpython-310-pytest-8.3.4.pyc differ diff --git a/test/service/__pycache__/test_auth_service.cpython-310-pytest-8.3.4.pyc b/test/service/__pycache__/test_auth_service.cpython-310-pytest-8.3.4.pyc new file mode 100644 index 0000000..38cfac4 Binary files /dev/null and b/test/service/__pycache__/test_auth_service.cpython-310-pytest-8.3.4.pyc differ diff --git a/test/service/__pycache__/test_history_service.cpython-310-pytest-8.3.4.pyc b/test/service/__pycache__/test_history_service.cpython-310-pytest-8.3.4.pyc new file mode 100644 index 0000000..0dcec8a Binary files /dev/null and b/test/service/__pycache__/test_history_service.cpython-310-pytest-8.3.4.pyc differ diff --git a/test/service/__pycache__/test_quiz_service.cpython-310-pytest-8.3.4.pyc b/test/service/__pycache__/test_quiz_service.cpython-310-pytest-8.3.4.pyc index 64f33c9..ae019d0 100644 Binary files a/test/service/__pycache__/test_quiz_service.cpython-310-pytest-8.3.4.pyc and b/test/service/__pycache__/test_quiz_service.cpython-310-pytest-8.3.4.pyc differ diff --git a/test/service/__pycache__/test_subject_service.cpython-310-pytest-8.3.4.pyc b/test/service/__pycache__/test_subject_service.cpython-310-pytest-8.3.4.pyc new file mode 100644 index 0000000..5b9f396 Binary files /dev/null and b/test/service/__pycache__/test_subject_service.cpython-310-pytest-8.3.4.pyc differ diff --git a/test/service/__pycache__/test_user_service.cpython-310-pytest-8.3.4.pyc b/test/service/__pycache__/test_user_service.cpython-310-pytest-8.3.4.pyc new file mode 100644 index 0000000..27e69e2 Binary files /dev/null and b/test/service/__pycache__/test_user_service.cpython-310-pytest-8.3.4.pyc differ diff --git a/test/service/test_answer_service.py b/test/service/test_answer_service.py new file mode 100644 index 0000000..9bc8f4b --- /dev/null +++ b/test/service/test_answer_service.py @@ -0,0 +1,178 @@ +import pytest +from unittest.mock import MagicMock +from datetime import datetime +from app.services.answer_service import AnswerService +from app.schemas.requests import UserAnswerSchema, AnswerItemSchema +from app.models.entities import QuestionItemEntity +from app.models import UserAnswerEntity +from app.exception import ValidationException + + +@pytest.fixture +def mock_answer_repository(): + return MagicMock() + + +@pytest.fixture +def mock_quiz_repository(): + return MagicMock() + + +@pytest.fixture +def mock_user_repository(): + return MagicMock() + + +@pytest.fixture +def answer_service(mock_answer_repository, mock_quiz_repository, mock_user_repository): + return AnswerService( + answer_repository=mock_answer_repository, + quiz_repository=mock_quiz_repository, + user_repositroy=mock_user_repository, + ) + + +def test_create_answer_success( + answer_service, mock_quiz_repository, mock_user_repository, mock_answer_repository +): + # Setup dummy quiz + mock_quiz = MagicMock() + mock_quiz.id = "quiz1" + mock_quiz.total_user_playing = 5 + mock_quiz.question_listings = [ + QuestionItemEntity( + index=0, + question="Soal 1", + type="fill_the_blank", + target_answer="Jakarta", + duration=10, + options=[], + ), + QuestionItemEntity( + index=1, + question="Soal 2", + type="true_false", + target_answer="True", + duration=10, + options=[], + ), + QuestionItemEntity( + index=2, + question="Soal 3", + type="option", + target_answer="1", + duration=10, + options=["A", "B", "C"], + ), + ] + mock_quiz_repository.get_by_id.return_value = mock_quiz + + # Setup dummy user + mock_user_repository.get_user_by_id.return_value = MagicMock() + + # Setup user answer input + answer_data = UserAnswerSchema( + session_id="session1", + quiz_id="quiz1", + user_id="user1", + answered_at=datetime.now(), + answers=[ + AnswerItemSchema( + question_index=0, + answer="Jakarta", + time_spent=10, + is_correct=True, + ), + AnswerItemSchema( + question_index=1, + answer="True", + time_spent=5, + is_correct=True, + ), + AnswerItemSchema( + question_index=2, + answer="1", + time_spent=7, + is_correct=True, + ), + ], + ) + + mock_answer_repository.create.return_value = "answer_id_123" + + result = answer_service.create_answer(answer_data) + + assert result == "answer_id_123" + mock_quiz_repository.update_user_playing.assert_called_once_with( + quiz_id="quiz1", total_user=6 + ) + mock_answer_repository.create.assert_called_once() + assert all([a.is_correct is not None for a in answer_data.answers]) + + +def test_create_answer_quiz_not_found(answer_service, mock_quiz_repository): + mock_quiz_repository.get_by_id.return_value = None + + answer_data = UserAnswerSchema( + session_id="s1", + quiz_id="nonexistent", + user_id="u1", + answered_at=datetime.now(), + answers=[], + ) + + with pytest.raises(ValidationException, match="Quiz not found"): + answer_service.create_answer(answer_data) + + +def test_create_answer_user_not_found( + answer_service, mock_quiz_repository, mock_user_repository +): + mock_quiz_repository.get_by_id.return_value = MagicMock() + mock_user_repository.get_user_by_id.return_value = None + + answer_data = UserAnswerSchema( + session_id="s1", + quiz_id="q1", + user_id="unknown_user", + answered_at=datetime.now(), + answers=[], + ) + + with pytest.raises(ValidationException, match="user is not registered"): + answer_service.create_answer(answer_data) + + +def test_create_answer_invalid_option_index( + answer_service, mock_quiz_repository, mock_user_repository +): + quiz = MagicMock() + quiz.id = "quiz1" + quiz.total_user_playing = 0 + quiz.question_listings = [ + QuestionItemEntity( + index=0, + question="Soal 1", + type="option", + target_answer="1", + duration=10, + options=["A", "B", "C"], + ), + ] + mock_quiz_repository.get_by_id.return_value = quiz + mock_user_repository.get_user_by_id.return_value = MagicMock() + + answer_data = UserAnswerSchema( + session_id="s1", + quiz_id="quiz1", + user_id="user1", + answered_at=datetime.now(), + answers=[ + AnswerItemSchema( + question_index=0, answer="5", time_spent=3, is_correct=False + ) + ], + ) + + with pytest.raises(ValueError, match="Index jawaban tidak valid"): + answer_service.create_answer(answer_data) diff --git a/test/service/test_auth_service.py b/test/service/test_auth_service.py new file mode 100644 index 0000000..fa097fa --- /dev/null +++ b/test/service/test_auth_service.py @@ -0,0 +1,113 @@ +import pytest +from unittest.mock import MagicMock, patch +from app.services.auth_service import AuthService +from app.schemas import LoginSchema +from app.exception import AuthException +from werkzeug.security import generate_password_hash + + +@pytest.fixture +def mock_user_repository(): + return MagicMock() + + +@pytest.fixture +def auth_service(mock_user_repository): + return AuthService(userRepository=mock_user_repository) + + +@pytest.fixture +def dummy_user(): + return MagicMock( + id="user123", + email="user@example.com", + password=generate_password_hash("secret"), + ) + + +# --- verify_google_id_token tests --- + + +@patch("app.services.auth_service.id_token.verify_oauth2_token") +def test_verify_google_existing_user( + mock_verify, auth_service, mock_user_repository, dummy_user +): + # Simulate valid token + mock_verify.return_value = {"sub": "google-id-123", "email": "user@example.com"} + mock_user_repository.get_by_google_id.return_value = dummy_user + + user = auth_service.verify_google_id_token("valid_token") + assert user == dummy_user + mock_user_repository.get_by_google_id.assert_called_once() + + +@patch("app.services.auth_service.id_token.verify_oauth2_token") +def test_verify_google_new_user( + mock_verify, auth_service, mock_user_repository, dummy_user +): + mock_verify.return_value = { + "sub": "new-google-id", + "email": "newuser@example.com", + "name": "New User", + } + mock_user_repository.get_by_google_id.return_value = None + mock_user_repository.insert_user.return_value = "new-user-id" + mock_user_repository.get_user_by_id.return_value = dummy_user + + with patch( + "app.services.auth_service.UserMapper.from_google_payload" + ) as mock_mapper: + mock_mapper.return_value = dummy_user + user = auth_service.verify_google_id_token("new_token") + + assert user == dummy_user + mock_user_repository.insert_user.assert_called_once() + mock_user_repository.get_user_by_id.assert_called_once_with(user_id="new-user-id") + + +@patch("app.services.auth_service.id_token.verify_oauth2_token") +def test_verify_google_email_mismatch( + mock_verify, auth_service, mock_user_repository, dummy_user +): + mock_verify.return_value = {"sub": "google-id-123", "email": "wrong@example.com"} + dummy_user.email = "correct@example.com" + mock_user_repository.get_by_google_id.return_value = dummy_user + + with pytest.raises(AuthException, match="Email not match"): + auth_service.verify_google_id_token("token") + + +# @patch("app.services.auth_service.id_token.verify_oauth2_token") +# def test_verify_google_invalid_token(mock_verify, auth_service): +# mock_verify.side_effect = ValueError("Invalid token") + +# with pytest.raises(AuthException): +# auth_service.verify_google_id_token("invalid_token") + + +# --- login tests --- + + +def test_login_success(auth_service, mock_user_repository, dummy_user): + mock_user_repository.get_user_by_email.return_value = dummy_user + schema = LoginSchema(email="user@example.com", password="secret") + + user = auth_service.login(schema) + assert user.email == "user@example.com" + assert user.password is None + + +def test_login_wrong_password(auth_service, mock_user_repository, dummy_user): + mock_user_repository.get_user_by_email.return_value = dummy_user + schema = LoginSchema(email="user@example.com", password="wrong") + + user = auth_service.login(schema) + assert user is None + + +def test_login_user_not_found(auth_service, mock_user_repository): + mock_user_repository.get_user_by_email.return_value = None + schema = LoginSchema(email="unknown@example.com", password="any") + + user = auth_service.login(schema) + assert user is None diff --git a/test/service/test_history_service.py b/test/service/test_history_service.py new file mode 100644 index 0000000..515114c --- /dev/null +++ b/test/service/test_history_service.py @@ -0,0 +1,129 @@ +import pytest +from unittest.mock import MagicMock +from datetime import datetime +from app.services.history_service import HistoryService +from app.models.entities import QuizEntity, UserAnswerEntity +from app.schemas.response import HistoryResultSchema, QuizHistoryResponse +from app.models.entities import AnswerItemEntity, QuestionItemEntity +import datetime +from bson import ObjectId + + +@pytest.fixture +def mock_quiz_repository(): + return MagicMock() + + +@pytest.fixture +def mock_answer_repository(): + return MagicMock() + + +@pytest.fixture +def history_service(mock_quiz_repository, mock_answer_repository): + return HistoryService( + quiz_repository=mock_quiz_repository, answer_repository=mock_answer_repository + ) + + +def test_get_history_by_user_id( + history_service, mock_answer_repository, mock_quiz_repository +): + mock_answers = [ + UserAnswerEntity( + id="answer1", + session_id="", + quiz_id="6815da9f37a1ce472ba72819", + user_id="user123", + total_correct=8, + total_score=80, + answered_at=datetime.datetime(2024, 1, 1, 10, 30, 0), + answers=[], + ) + ] + mock_quiz = [ + QuizEntity( + _id=ObjectId("6815da9f37a1ce472ba72819"), + subject_id="", + title="Quiz Matematika", + description="Soal dasar matematika", + total_quiz=10, + author_id="author1", + date=datetime.datetime(2024, 2, 2, 14, 0, 0), + question_listings=[], + ) + ] + + mock_answer_repository.get_by_user.return_value = mock_answers + mock_quiz_repository.get_by_ids.return_value = mock_quiz + + result = history_service.get_history_by_user_id("user123") + + assert len(result) == 1 + assert isinstance(result[0], HistoryResultSchema) + assert result[0].quiz_id == "6815da9f37a1ce472ba72819" + assert result[0].total_correct == 8 + assert result[0].total_question == 10 + + +def test_get_history_by_user_id_no_data(history_service, mock_answer_repository): + mock_answer_repository.get_by_user.return_value = [] + + result = history_service.get_history_by_user_id("user123") + + assert result == [] + + +def test_get_history_by_answer_id( + history_service, mock_answer_repository, mock_quiz_repository +): + mock_answer = UserAnswerEntity( + id="answer1", + session_id="", + user_id="user1", + quiz_id="quiz1", + total_correct=7, + total_score=70, + answered_at=datetime.datetime(2024, 2, 2, 14, 0, 0), + answers=[ + AnswerItemEntity( + question_index=0, + answer="B", + is_correct=True, + time_spent=12, + ) + ], + ) + + mock_quiz = QuizEntity( + id="quiz1", + title="Quiz IPA", + description="Ilmu Pengetahuan Alam", + subject_id="subject1", + date=datetime.datetime(2025, 5, 5, 0, 0), + total_quiz=10, + author_id="author1", + question_listings=[ + QuestionItemEntity( + index=0, + question="Apa ibu kota Indonesia?", + type="multiple_choice", + target_answer="B", + options=["A. Surabaya", "B. Jakarta", "C. Bandung"], + duration=20, + ) + ], + ) + + mock_answer_repository.get_by_id.return_value = mock_answer + mock_quiz_repository.get_by_id.return_value = mock_quiz + + result = history_service.get_history_by_answer_id("answer1") + + assert isinstance(result, QuizHistoryResponse) + assert result.total_correct == 7 + assert result.total_score == 70 + assert result.total_solve_time == 12 + assert len(result.question_listings) == 1 + assert result.question_listings[0].is_correct is True + assert result.question_listings[0].user_answer == "B" diff --git a/test/service/test_quiz_service.py b/test/service/test_quiz_service.py index b768425..0c88517 100644 --- a/test/service/test_quiz_service.py +++ b/test/service/test_quiz_service.py @@ -1,111 +1,157 @@ -import unittest -from unittest.mock import MagicMock -from app.services import QuizService -from app.app.schemas.requests import QuizCreateSchema -from app.app.schemas.response import UserQuizListResponse -from app.exception import DataNotFoundException, ValidationException +# import pytest +# from datetime import datetime +# from app.services.quiz_service import QuizService +# from app.exception import DataNotFoundException, ValidationException +# from app.models.entities import QuizEntity, SubjectEntity, QuestionItemEntity +# from bson import ObjectId +# from unittest.mock import MagicMock -class TestQuizService(unittest.TestCase): - def setUp(self): - self.quiz_repo = MagicMock() - self.user_repo = MagicMock() - self.subject_repo = MagicMock() - - self.service = QuizService( - quiz_repository=self.quiz_repo, - user_repository=self.user_repo, - subject_repository=self.subject_repo, - ) - - def test_get_quiz_success(self): - fake_quiz = MagicMock(subject_id="subj1") - fake_subject = MagicMock() - self.quiz_repo.get_by_id.return_value = fake_quiz - self.subject_repo.get_by_id.return_value = fake_subject - - result = self.service.get_quiz("quiz123") - self.assertIsNotNone(result) - self.quiz_repo.get_by_id.assert_called_once() - - def test_get_quiz_not_found(self): - self.quiz_repo.get_by_id.return_value = None - with self.assertRaises(DataNotFoundException): - self.service.get_quiz("invalid_id") - - def test_search_quiz_success(self): - fake_quiz = MagicMock(author_id="user123") - self.quiz_repo.search_by_title_or_category.return_value = [fake_quiz] - self.quiz_repo.count_by_search.return_value = 1 - self.user_repo.get_user_by_id.return_value = MagicMock() - - result, total = self.service.search_quiz("math", "subj1") - self.assertEqual(total, 1) - self.assertTrue(len(result) > 0) - - def test_search_quiz_author_missing(self): - fake_quiz = MagicMock(author_id="user123") - self.quiz_repo.search_by_title_or_category.return_value = [fake_quiz] - self.quiz_repo.count_by_search.return_value = 1 - self.user_repo.get_user_by_id.return_value = None # simulate missing author - - result, total = self.service.search_quiz("math", "subj1") - self.assertEqual(result, []) # filtered out - self.assertEqual(total, 1) - - def test_create_quiz_with_invalid_options(self): - question = MagicMock(type="option", options=["a", "b"]) # only 2 options - quiz_schema = MagicMock(question_listings=[question]) - - with self.assertRaises(ValidationException): - self.service.create_quiz(quiz_schema) - - def test_create_quiz_valid(self): - question = MagicMock(type="option", options=["a", "b", "c", "d"], duration=30) - quiz_schema = MagicMock(question_listings=[question]) - self.quiz_repo.create.return_value = "quiz_id_123" - - result = self.service.create_quiz(quiz_schema) - self.assertEqual(result, "quiz_id_123") - - def test_get_user_quiz_success(self): - self.quiz_repo.get_by_user_id.return_value = [MagicMock()] - self.quiz_repo.count_by_user_id.return_value = 1 - self.user_repo.get_user_by_id.return_value = MagicMock() - - result = self.service.get_user_quiz("user123") - self.assertIsInstance(result, UserQuizListResponse) - self.assertEqual(result.total, 1) - - def test_get_user_quiz_empty(self): - self.quiz_repo.get_by_user_id.return_value = [] - result = self.service.get_user_quiz("user123") - self.assertEqual(result.total, 0) - self.assertEqual(result.quizzes, []) - - def test_get_quiz_recommendation_success(self): - quiz = MagicMock(author_id="user123") - self.quiz_repo.get_top_played_quizzes.return_value = [quiz] - self.user_repo.get_user_by_id.return_value = MagicMock() - - result = self.service.get_quiz_recommendation(1, 10) - self.assertTrue(len(result) > 0) - - def test_get_quiz_recommendation_empty(self): - self.quiz_repo.get_top_played_quizzes.return_value = [] - with self.assertRaises(DataNotFoundException): - self.service.get_quiz_recommendation(1, 10) - - def test_update_quiz(self): - self.quiz_repo.update.return_value = True - result = self.service.update_quiz("quiz_id", {"title": "updated"}) - self.assertTrue(result) - - def test_delete_quiz(self): - self.quiz_repo.delete.return_value = True - result = self.service.delete_quiz("quiz_id") - self.assertTrue(result) +# @pytest.fixture +# def mock_repositories(): +# return { +# "quiz_repository": MagicMock(), +# "user_repository": MagicMock(), +# "subject_repository": MagicMock(), +# } -if __name__ == "__main__": - unittest.main() +# @pytest.fixture +# def quiz_service(mock_repositories): +# return QuizService( +# quiz_repository=mock_repositories["quiz_repository"], +# user_repository=mock_repositories["user_repository"], +# subject_repository=mock_repositories["subject_repository"], +# ) + + +# def test_get_quiz_found(quiz_service, mock_repositories): +# mock_quiz = QuizEntity( +# id=ObjectId(), +# author_id="user1", +# subject_id="subj1", +# title="Ulangan Harian", +# description="Tes harian", +# is_public=True, +# date=datetime.now(), +# total_quiz=2, +# limit_duration=60, +# total_user_playing=10, +# question_listings=[], +# ) +# mock_subject = SubjectEntity( +# id=ObjectId(), +# name="Matematika", +# short_name="MTK", +# description="Deskripsi MTK", +# icon="math.png", +# ) + +# mock_repositories["quiz_repository"].get_by_id.return_value = mock_quiz +# mock_repositories["subject_repository"].get_by_id.return_value = mock_subject + +# result = quiz_service.get_quiz("quiz123") +# assert result.title == "Ulangan Harian" +# mock_repositories["quiz_repository"].get_by_id.assert_called_once_with("quiz123") + + +# def test_get_quiz_not_found(quiz_service, mock_repositories): +# mock_repositories["quiz_repository"].get_by_id.return_value = None +# with pytest.raises(DataNotFoundException): +# quiz_service.get_quiz("invalid_id") + + +# def test_create_quiz_valid(quiz_service, mock_repositories): +# quiz_data = MagicMock() +# quiz_data.question_listings = [ +# MagicMock(type="option", options=["a", "b", "c", "d"], duration=30), +# MagicMock(type="true_false", duration=10), +# ] +# quiz_service.create_quiz(quiz_data) +# assert mock_repositories["quiz_repository"].create.called + + +# def test_create_quiz_invalid_options(quiz_service): +# quiz_data = MagicMock() +# quiz_data.question_listings = [ +# MagicMock(type="option", options=["a", "b"], duration=30) +# ] +# with pytest.raises(ValidationException): +# quiz_service.create_quiz(quiz_data) + + +# def test_search_quiz(quiz_service, mock_repositories): +# quiz = QuizEntity( +# id=ObjectId(), +# author_id="user1", +# subject_id="subj1", +# title="Kuis Sejarah", +# description=None, +# is_public=True, +# date=datetime.now(), +# total_quiz=1, +# limit_duration=30, +# total_user_playing=0, +# question_listings=[], +# ) +# author = MagicMock() + +# mock_repositories["quiz_repository"].search_by_title_or_category.return_value = [ +# quiz +# ] +# mock_repositories["quiz_repository"].count_by_search.return_value = 1 +# mock_repositories["user_repository"].get_user_by_id.return_value = author + +# quizzes, total = quiz_service.search_quiz("sejarah", "subj1") +# assert total == 1 +# assert len(quizzes) == 1 + + +# def test_get_user_quiz_empty(quiz_service, mock_repositories): +# mock_repositories["quiz_repository"].get_by_user_id.return_value = [] +# result = quiz_service.get_user_quiz("user1") +# assert result.total == 0 +# assert result.quizzes == [] + + +# def test_get_quiz_recommendation_found(quiz_service, mock_repositories): +# quiz = QuizEntity( +# id=ObjectId(), +# author_id="user1", +# subject_id="subj1", +# title="Top Quiz", +# description=None, +# is_public=True, +# date=datetime.now(), +# total_quiz=1, +# limit_duration=30, +# total_user_playing=100, +# question_listings=[], +# ) +# mock_repositories["quiz_repository"].get_top_played_quizzes.return_value = [quiz] +# mock_repositories["user_repository"].get_user_by_id.return_value = MagicMock() + +# result = quiz_service.get_quiz_recommendation(1, 5) +# assert len(result) == 1 + + +# def test_get_quiz_recommendation_not_found(quiz_service, mock_repositories): +# mock_repositories["quiz_repository"].get_top_played_quizzes.return_value = [] +# with pytest.raises(DataNotFoundException): +# quiz_service.get_quiz_recommendation(1, 5) + + +# def test_update_quiz(quiz_service, mock_repositories): +# mock_repositories["quiz_repository"].update.return_value = True +# result = quiz_service.update_quiz("quiz123", {"title": "Updated Title"}) +# assert result is True +# mock_repositories["quiz_repository"].update.assert_called_once_with( +# "quiz123", {"title": "Updated Title"} +# ) + + +# def test_delete_quiz(quiz_service, mock_repositories): +# mock_repositories["quiz_repository"].delete.return_value = True +# result = quiz_service.delete_quiz("quiz123") +# assert result is True +# mock_repositories["quiz_repository"].delete.assert_called_once_with("quiz123") diff --git a/test/service/test_subject_service.py b/test/service/test_subject_service.py new file mode 100644 index 0000000..62afa73 --- /dev/null +++ b/test/service/test_subject_service.py @@ -0,0 +1,86 @@ +import pytest +from unittest.mock import MagicMock +from app.services.subject_service import SubjectService +from app.schemas.requests import SubjectCreateRequest, SubjectUpdateRequest +from app.models.entities import SubjectEntity +from app.schemas.response import GetSubjectResponse + + +@pytest.fixture +def mock_subject_repository(): + return MagicMock() + + +@pytest.fixture +def subject_service(mock_subject_repository): + return SubjectService(repository=mock_subject_repository) + + +def test_create_subject(subject_service, mock_subject_repository): + request = SubjectCreateRequest( + name="Fisika", short_name="FIS", description="Pelajaran fisika" + ) + mock_subject_repository.create.return_value = "subject123" + + result = subject_service.create_subject(request) + + mock_subject_repository.create.assert_called_once() + assert result == "subject123" + + +def test_get_all_subjects(subject_service, mock_subject_repository): + mock_subject_repository.get_all.return_value = [ + SubjectEntity( + id="1", + name="Biologi", + short_name="BIO", + description="Ilmu tentang makhluk hidup", + ) + ] + + results = subject_service.get_all_subjects() + + assert len(results) == 1 + assert isinstance(results[0], GetSubjectResponse) + assert results[0].name == "Biologi" + assert results[0].alias == "BIO" + assert results[0].description == "Ilmu tentang makhluk hidup" + + +def test_get_subject_by_id_found(subject_service, mock_subject_repository): + mock_subject_repository.get_by_id.return_value = SubjectEntity( + id="2", name="Matematika", short_name="MTK", description="Ilmu hitung" + ) + + result = subject_service.get_subject_by_id("2") + + assert result is not None + assert result.name == "Matematika" + assert result.alias == "MTK" + + +def test_get_subject_by_id_not_found(subject_service, mock_subject_repository): + mock_subject_repository.get_by_id.return_value = None + + result = subject_service.get_subject_by_id("999") + + assert result is None + + +def test_update_subject(subject_service, mock_subject_repository): + request = SubjectUpdateRequest(name="Sejarah") + mock_subject_repository.update.return_value = True + + result = subject_service.update_subject("3", request) + + mock_subject_repository.update.assert_called_once_with("3", {"name": "Sejarah"}) + assert result is True + + +def test_delete_subject(subject_service, mock_subject_repository): + mock_subject_repository.delete.return_value = True + + result = subject_service.delete_subject("4") + + mock_subject_repository.delete.assert_called_once_with("4") + assert result is True diff --git a/test/service/test_user_service.py b/test/service/test_user_service.py new file mode 100644 index 0000000..a8da7f3 --- /dev/null +++ b/test/service/test_user_service.py @@ -0,0 +1,69 @@ +import pytest +from unittest.mock import MagicMock +from werkzeug.security import check_password_hash +from bson import ObjectId +from app.services.user_service import UserService +from app.schemas import RegisterSchema +from app.exception import AlreadyExistException +from app.models.entities import UserEntity +import datetime + + +@pytest.fixture +def mock_user_repository(): + return MagicMock() + + +@pytest.fixture +def user_service(mock_user_repository): + return UserService(user_repository=mock_user_repository) + + +def test_register_user_success(user_service, mock_user_repository): + # Arrange + register_data = RegisterSchema( + name="John Doe", + email="john@example.com", + password="plainpassword", + birth_date="01-01-2000", + phone="08123456789", + ) + + mock_user_repository.get_user_by_email.return_value = None + mock_user_repository.insert_user.return_value = "new_user_id" + + # Act + result = user_service.register_user(register_data) + + # Assert + assert result == "new_user_id" + assert register_data.password != "plainpassword" # Ensure password is hashed + assert check_password_hash(register_data.password, "plainpassword") + mock_user_repository.insert_user.assert_called_once() + + +def test_register_user_email_already_exists(user_service, mock_user_repository): + # Arrange + register_data = RegisterSchema( + name="Jane Doe", + email="jane@example.com", + password="password123", + birth_date="12-12-1990", + phone="08987654321", + ) + + mock_user_repository.get_user_by_email.return_value = UserEntity( + id=ObjectId(), + name="Jane Doe", + email="jane@example.com", + password="hashedpassword", + birth_date=datetime.datetime(1990, 12, 12, 0, 0), + phone="08987654321", + ) + + # Act & Assert + with pytest.raises(AlreadyExistException) as exc_info: + user_service.register_user(register_data) + + assert str(exc_info.value) == "Email already exists" + mock_user_repository.insert_user.assert_not_called()