diff --git a/src/app.py b/src/app.py index 63ffeb1..b9920c7 100644 --- a/src/app.py +++ b/src/app.py @@ -3,12 +3,20 @@ from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from dotenv import load_dotenv +import logging from src.router import router load_dotenv() +# 로깅 설정 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler()] +) + origins = [ "https://d6jo3bhmz1u5k.cloudfront.net", "https://localhost:5173", diff --git a/src/main/ai/data/FileDuplicateCheckQueue.py b/src/main/ai/data/FileDuplicateCheckQueue.py new file mode 100644 index 0000000..2267b64 --- /dev/null +++ b/src/main/ai/data/FileDuplicateCheckQueue.py @@ -0,0 +1,27 @@ +import json + +class FileDuplicateCheckQueue: + def __init__(self, sqs_client, queue_url): + self.sqs_client = sqs_client + self.queue_url = queue_url + + def send_message(self, request_id: str, user_id: str, s3_bucket: str, s3_key: str): + """SQS 큐에 메시지를 전송합니다.""" + message_body = { + 'request_type': 'file_duplicate_check_embedding_file', + 'request_id': request_id, + 'user_id': user_id, + 'payload': { + 's3_bucket': s3_bucket, + 's3_key': s3_key + } + } + + response = self.sqs_client.send_message( + QueueUrl=self.queue_url, + MessageGroupId=str(user_id), + MessageDeduplicationId=str(request_id), + MessageBody=json.dumps(message_body), + ) + + return response \ No newline at end of file diff --git a/src/main/ai/data/FileDuplicateCheckRepository.py b/src/main/ai/data/FileDuplicateCheckRepository.py new file mode 100644 index 0000000..2d08087 --- /dev/null +++ b/src/main/ai/data/FileDuplicateCheckRepository.py @@ -0,0 +1,100 @@ +from bson import ObjectId +from datetime import datetime, timezone + + +class FileDuplicateCheckRepository: + def __init__(self, mongo_client): + self.mongo_client = mongo_client + self.db = mongo_client.get_database() + self.file_checks_collection = self.db.get_collection("file_duplicate_checks") + self.files_collection = self.db.get_collection("files") + self.file_embeddings_collection = self.db.get_collection("file_embeddings") + + def get_current_time(self): + """현재 시간을 UTC 기준으로 반환합니다.""" + return datetime.now(timezone.utc) + + def get_file_by_id(self, file_id: str): + """파일 ID로 파일 정보를 조회합니다.""" + try: + file_obj_id = ObjectId(file_id) + return self.files_collection.find_one({"_id": file_obj_id}) + except Exception as e: + return None + + def has_file_embedding(self, file_id: str) -> bool: + """파일의 임베딩 존재 여부를 확인합니다.""" + try: + file_obj_id = ObjectId(file_id) + result = self.file_embeddings_collection.find_one({"file_id": file_obj_id}) + return result is not None + except Exception as e: + return False + + def create_duplicate_check_request(self, file_id: str, user_id: str): + """중복 검사 요청을 생성합니다.""" + now = self.get_current_time() + document = { + "file_id": file_id, + "user_id": user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": now + } + + result = self.file_checks_collection.insert_one(document) + + return self.file_checks_collection.find_one({"_id": result.inserted_id}) + + def get_duplicate_check_by_file_id(self, file_id: str, user_id: str): + """파일 ID와 사용자 ID로 중복 검사 요청을 조회합니다.""" + return self.file_checks_collection.find_one({ + "file_id": file_id, + "user_id": user_id + }) + + def get_duplicate_check_by_id(self, request_id: str): + """요청 ID로 중복 검사 요청을 조회합니다.""" + try: + request_obj_id = ObjectId(request_id) + return self.file_checks_collection.find_one({"_id": request_obj_id}) + except Exception as e: + return None + + def update_file_duplicate_status(self, file_id: str, is_duplicated: bool): + """파일의 중복 상태를 업데이트합니다.""" + try: + file_obj_id = ObjectId(file_id) + result = self.files_collection.update_one( + {"_id": file_obj_id}, + {"$set": {"is_duplicated": is_duplicated}} + ) + + if result.modified_count > 0: + return self.files_collection.find_one({"_id": file_obj_id}) + return None + except Exception as e: + return None + + def update_duplicate_check_result(self, request_id: str, is_duplicated: bool): + """중복 검사 결과를 업데이트합니다.""" + try: + now = self.get_current_time() + request_obj_id = ObjectId(request_id) + + result = self.file_checks_collection.update_one( + {"_id": request_obj_id}, + { + "$set": { + "is_completed": True, + "is_duplicated": is_duplicated, + "updated_at": now + } + } + ) + + if result.modified_count > 0: + return self.file_checks_collection.find_one({"_id": request_obj_id}) + return None + except Exception as e: + return None \ No newline at end of file diff --git a/src/main/ai/data/__init__.py b/src/main/ai/data/__init__.py index e69de29..815c852 100644 --- a/src/main/ai/data/__init__.py +++ b/src/main/ai/data/__init__.py @@ -0,0 +1,4 @@ +from src.main.ai.data.CategoryRecommendationRepository import CategoryRecommendationRepository +from src.main.ai.data.CategoryRecommendationQueue import CategoryRecommendationQueue +from src.main.ai.data.FileDuplicateCheckRepository import FileDuplicateCheckRepository +from src.main.ai.data.FileDuplicateCheckQueue import FileDuplicateCheckQueue diff --git a/src/main/ai/di/dependencies.py b/src/main/ai/di/dependencies.py index b6a9c67..f5a2956 100644 --- a/src/main/ai/di/dependencies.py +++ b/src/main/ai/di/dependencies.py @@ -5,6 +5,9 @@ from src.main.ai.data.CategoryRecommendationRepository import CategoryRecommendationRepository from src.main.ai.service.CategoryRecommendationService import CategoryRecommendationService from src.main.ai.data.CategoryRecommendationQueue import CategoryRecommendationQueue +from src.main.ai.data.FileDuplicateCheckRepository import FileDuplicateCheckRepository +from src.main.ai.service.FileDuplicateCheckService import FileDuplicateCheckService +from src.main.ai.data.FileDuplicateCheckQueue import FileDuplicateCheckQueue from src.main.config.mongodb import get_mongo_client load_dotenv() @@ -31,3 +34,26 @@ def get_category_recommendation_service(): repository = get_category_recommendation_repository() queue = get_category_recommendation_queue() return CategoryRecommendationService(repository, queue) + + +def get_file_duplicate_check_repository(): + client = get_mongo_client() + return FileDuplicateCheckRepository(client) + + +def get_file_duplicate_check_queue(): + if os.getenv('ENV') == 'local': + aws_profile = os.getenv('AWS_PROFILE', 'default') + session = boto3.Session(profile_name=aws_profile) + sqs_client = session.client('sqs') + else: + sqs_client = boto3.client('sqs', region_name=os.getenv('AWS_REGION')) + + queue_url = os.getenv('SQS_REQUEST_QUEUE_URL') + return FileDuplicateCheckQueue(sqs_client, queue_url) + + +def get_file_duplicate_check_service(): + repository = get_file_duplicate_check_repository() + queue = get_file_duplicate_check_queue() + return FileDuplicateCheckService(repository, queue) diff --git a/src/main/ai/exception/BadRequestException.py b/src/main/ai/exception/BadRequestException.py new file mode 100644 index 0000000..7792b38 --- /dev/null +++ b/src/main/ai/exception/BadRequestException.py @@ -0,0 +1,13 @@ +from fastapi import HTTPException + + +class BadRequestException(HTTPException): + """ + 요청이 잘못되었을 때 발생하는 예외 + """ + + def __init__(self, detail: str = "Bad request"): + super().__init__( + status_code=400, + detail=detail + ) \ No newline at end of file diff --git a/src/main/ai/exception/FileNotFoundException.py b/src/main/ai/exception/FileNotFoundException.py new file mode 100644 index 0000000..a5ff576 --- /dev/null +++ b/src/main/ai/exception/FileNotFoundException.py @@ -0,0 +1,13 @@ +from fastapi import HTTPException + + +class FileNotFoundException(HTTPException): + """ + 파일을 찾을 수 없을 때 발생하는 예외 + """ + + def __init__(self, file_id: str): + super().__init__( + status_code=404, + detail=f"File with ID '{file_id}' not found" + ) \ No newline at end of file diff --git a/src/main/ai/exception/__init__.py b/src/main/ai/exception/__init__.py new file mode 100644 index 0000000..b25c7c4 --- /dev/null +++ b/src/main/ai/exception/__init__.py @@ -0,0 +1,3 @@ +""" +예외 패키지 +""" \ No newline at end of file diff --git a/src/main/ai/models/CategoryRecommendation.py b/src/main/ai/models/CategoryRecommendation.py index 06b8c91..e20e032 100644 --- a/src/main/ai/models/CategoryRecommendation.py +++ b/src/main/ai/models/CategoryRecommendation.py @@ -18,4 +18,5 @@ class CategoryRecommendationStatusResponse(BaseModel): class CategoryRecommendationResultRequest(BaseModel): + request_id: Optional[str] = None predicted_category: str \ No newline at end of file diff --git a/src/main/ai/models/FileDuplicateCheck.py b/src/main/ai/models/FileDuplicateCheck.py new file mode 100644 index 0000000..e9950b6 --- /dev/null +++ b/src/main/ai/models/FileDuplicateCheck.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime + + +class FileDuplicateCheckRequest(BaseModel): + """파일 중복 검사 요청 모델""" + user_id: str + file_id: str + + +class FileDuplicateCheckResponse(BaseModel): + """파일 중복 검사 응답 모델""" + request_id: str + + +class FileDuplicateCheckStatusResponse(BaseModel): + """파일 중복 검사 상태 응답 모델""" + request_id: str + file_id: str + is_completed: bool + is_duplicated: Optional[bool] = None + + +class FileDuplicateCheckResultRequest(BaseModel): + """파일 중복 검사 결과 요청 모델""" + request_id: str + is_duplicated: bool + + +class FileDuplicateCheckEmbeddingsRequest(BaseModel): + """파일 임베딩 저장 요청 모델""" + file_id: str + embeddings: List[float] \ No newline at end of file diff --git a/src/main/ai/models/__init__.py b/src/main/ai/models/__init__.py index 0519ecb..4334c6f 100644 --- a/src/main/ai/models/__init__.py +++ b/src/main/ai/models/__init__.py @@ -1 +1,2 @@ - \ No newline at end of file +from src.main.ai.models.CategoryRecommendation import * +from src.main.ai.models.FileDuplicateCheck import * \ No newline at end of file diff --git a/src/main/ai/router/AIInternalAPIRouter.py b/src/main/ai/router/AIInternalAPIRouter.py index 59f7578..e20fee3 100644 --- a/src/main/ai/router/AIInternalAPIRouter.py +++ b/src/main/ai/router/AIInternalAPIRouter.py @@ -1,9 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import JSONResponse -from src.main.ai.di.dependencies import get_category_recommendation_service +from src.main.ai.di.dependencies import get_category_recommendation_service, get_file_duplicate_check_service from src.main.ai.models.CategoryRecommendation import CategoryRecommendationResultRequest, CategoryRecommendationStatusResponse from src.main.ai.service.CategoryRecommendationService import CategoryRecommendationService +from src.main.ai.models.FileDuplicateCheck import FileDuplicateCheckStatusResponse, FileDuplicateCheckEmbeddingsRequest, FileDuplicateCheckRequest, FileDuplicateCheckResponse, FileDuplicateCheckResultRequest +from src.main.ai.service.FileDuplicateCheckService import FileDuplicateCheckService router = APIRouter( @@ -12,13 +14,39 @@ ) -@router.post("/category-recommendation-results/{request_id}", response_model=CategoryRecommendationStatusResponse) +@router.post("/category-recommendation-results", response_model=dict) async def update_category_recommendation_result( - request_id: str, request: CategoryRecommendationResultRequest, service: CategoryRecommendationService = Depends(get_category_recommendation_service) ): - result = service.update_recommendation_result(request_id, request) + result = service.update_recommendation_result(request.request_id, request) + + if not result: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={ + "detail": "요청을 찾을 수 없습니다. 존재하지 않는 ID입니다." + } + ) + + return {"success": True} + + +@router.post("/file-duplicate-checks", response_model=FileDuplicateCheckResponse) +async def create_file_duplicate_check( + request: FileDuplicateCheckRequest, + service: FileDuplicateCheckService = Depends(get_file_duplicate_check_service) +): + result = service.create_duplicate_check_request(request) + return result + + +@router.post("/file-duplicate-check-embeddings", response_model=dict) +async def update_file_duplicate_check_result( + request: FileDuplicateCheckResultRequest, + service: FileDuplicateCheckService = Depends(get_file_duplicate_check_service) +): + result = service.update_duplicate_check_result(request.request_id, request.is_duplicated) if not result: return JSONResponse( @@ -29,5 +57,13 @@ async def update_category_recommendation_result( "detail": "존재하지 않는 ID입니다." } ) - - return result + + # 요청 정보 조회 + check = service.repository.get_duplicate_check_by_id(request.request_id) + + return { + "request_id": request.request_id, + "file_id": check["file_id"], + "is_completed": check["is_completed"], + "is_duplicated": check["is_duplicated"] + } diff --git a/src/main/ai/router/AIPublicAPIRouter.py b/src/main/ai/router/AIPublicAPIRouter.py index 7c4cdf4..e507e74 100644 --- a/src/main/ai/router/AIPublicAPIRouter.py +++ b/src/main/ai/router/AIPublicAPIRouter.py @@ -2,9 +2,11 @@ import uuid from src.main.auth.dependencies import get_current_user -from src.main.ai.di.dependencies import get_category_recommendation_service +from src.main.ai.di.dependencies import get_category_recommendation_service, get_file_duplicate_check_service from src.main.ai.models.CategoryRecommendation import CategoryRecommendationRequest, CategoryRecommendationResponse, CategoryRecommendationStatusResponse from src.main.ai.service.CategoryRecommendationService import CategoryRecommendationService +from src.main.ai.models.FileDuplicateCheck import FileDuplicateCheckStatusResponse +from src.main.ai.service.FileDuplicateCheckService import FileDuplicateCheckService router = APIRouter( @@ -43,3 +45,23 @@ async def get_category_recommendation_status( ) return result + + +@router.get("/file-duplicate-checks", response_model=FileDuplicateCheckStatusResponse) +async def get_file_duplicate_check_status( + file_id: str, + user_id: uuid.UUID = Depends(get_current_user), + service: FileDuplicateCheckService = Depends(get_file_duplicate_check_service) +): + """ + 파일 중복 검사 결과 조회 + """ + result = service.get_duplicate_check_status(file_id, str(user_id)) + + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="요청을 찾을 수 없습니다. 존재하지 않는 ID입니다." + ) + + return result diff --git a/src/main/ai/router/__init__.py b/src/main/ai/router/__init__.py index e69de29..2247572 100644 --- a/src/main/ai/router/__init__.py +++ b/src/main/ai/router/__init__.py @@ -0,0 +1,2 @@ +from src.main.ai.router.AIPublicAPIRouter import router as public_router +from src.main.ai.router.AIInternalAPIRouter import router as internal_router diff --git a/src/main/ai/service/FileDuplicateCheckService.py b/src/main/ai/service/FileDuplicateCheckService.py new file mode 100644 index 0000000..ce0b6c1 --- /dev/null +++ b/src/main/ai/service/FileDuplicateCheckService.py @@ -0,0 +1,118 @@ +from fastapi import HTTPException, status +from bson import ObjectId +import json +import logging + +from src.main.ai.models.FileDuplicateCheck import ( + FileDuplicateCheckRequest, + FileDuplicateCheckResponse, + FileDuplicateCheckStatusResponse, + FileDuplicateCheckResultRequest, + FileDuplicateCheckEmbeddingsRequest +) + +# 로거 설정 +logger = logging.getLogger(__name__) + + +class FileDuplicateCheckService: + def __init__(self, repository, sqs_service): + self.repository = repository + self.sqs_service = sqs_service + + def create_duplicate_check_request(self, request: FileDuplicateCheckRequest) -> FileDuplicateCheckResponse: + """ + 파일 중복 검사 요청을 생성하고 SQS에 메시지를 발송합니다. + """ + # 1. 파일 존재 여부 확인 + file = self.repository.get_file_by_id(request.file_id) + if not file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="파일을 찾을 수 없습니다. 존재하지 않는 ID입니다." + ) + + # 2. 중복 검사 결과가 있는지 확인 + existing_check = self.repository.get_duplicate_check_by_file_id(request.file_id, request.user_id) + if existing_check: + logger.info(f"이미 중복 검사 요청이 존재합니다. request_id: {str(existing_check['_id'])}, file_id: {request.file_id}, user_id: {request.user_id}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="이미 중복 검사 요청이 존재합니다." + ) + + # 3. 중복 검사 요청 생성 + result = self.repository.create_duplicate_check_request( + file_id=request.file_id, + user_id=request.user_id + ) + + # 4. SQS에 메시지 발송 + file_data = { + "s3_bucket": file["s3_bucket"], + "s3_key": file["s3_key"] + } + + message = { + "request_type": "file_duplicate_check_embedding_file", + "request_id": str(result["_id"]), + "user_id": request.user_id, + "payload": file_data + } + + message_json = json.dumps(message) + logger.info(f"SQS 메시지 발송: {message_json}") + response = self.sqs_service.send_message(message_json) + logger.info(f"SQS 메시지 발송 응답: {response}") + + # 5. 요청 ID 응답 + return FileDuplicateCheckResponse(request_id=str(result["_id"])) + + def get_duplicate_check_status(self, file_id: str, user_id: str) -> FileDuplicateCheckStatusResponse: + """ + 파일 중복 검사 상태를 조회합니다. + """ + # 1. 중복 검사 요청 조회 + check = self.repository.get_duplicate_check_by_file_id(file_id, user_id) + + # 2. 요청이 없으면 None 반환 + if not check: + logger.info(f"중복 검사 요청을 찾을 수 없습니다. file_id: {file_id}, user_id: {user_id}") + return None + + # 3. 요청이 있으면 상태 반환 + logger.info(f"중복 검사 요청을 찾았습니다. request_id: {str(check['_id'])}, is_completed: {check['is_completed']}") + return FileDuplicateCheckStatusResponse( + request_id=str(check["_id"]), + file_id=check["file_id"], + is_completed=check["is_completed"], + is_duplicated=check["is_duplicated"] + ) + + def update_duplicate_check_result(self, request_id: str, is_duplicated: bool) -> bool: + """ + 파일 중복 검사 결과를 업데이트합니다. + """ + # 1. 중복 검사 요청 조회 + check = self.repository.get_duplicate_check_by_id(request_id) + + # 2. 요청이 없으면 False 반환 + if not check: + logger.info(f"중복 검사 요청을 찾을 수 없습니다. request_id: {request_id}") + return False + + # 3. 파일 중복 상태 업데이트 + logger.info(f"파일 중복 상태 업데이트 시작. file_id: {check['file_id']}, is_duplicated: {is_duplicated}") + file_result = self.repository.update_file_duplicate_status(check["file_id"], is_duplicated) + + # 4. 중복 검사 결과 업데이트 + logger.info(f"중복 검사 결과 업데이트 시작. request_id: {request_id}, is_duplicated: {is_duplicated}") + result = self.repository.update_duplicate_check_result( + request_id=request_id, + is_duplicated=is_duplicated + ) + + # 5. 업데이트 성공 여부 반환 + is_success = result is not None + logger.info(f"중복 검사 결과 업데이트 결과: {is_success}") + return is_success \ No newline at end of file diff --git a/src/main/ai/service/__init__.py b/src/main/ai/service/__init__.py index e69de29..12c5f3f 100644 --- a/src/main/ai/service/__init__.py +++ b/src/main/ai/service/__init__.py @@ -0,0 +1,2 @@ +from src.main.ai.service.CategoryRecommendationService import CategoryRecommendationService +from src.main.ai.service.FileDuplicateCheckService import FileDuplicateCheckService diff --git a/src/tests/ai/__init__.py b/src/tests/ai/__init__.py index e69de29..b28b04f 100644 --- a/src/tests/ai/__init__.py +++ b/src/tests/ai/__init__.py @@ -0,0 +1,3 @@ + + + diff --git a/src/tests/ai/data/__init__.py b/src/tests/ai/data/__init__.py index e69de29..b28b04f 100644 --- a/src/tests/ai/data/__init__.py +++ b/src/tests/ai/data/__init__.py @@ -0,0 +1,3 @@ + + + diff --git a/src/tests/ai/data/test_file_duplicate_check_queue.py b/src/tests/ai/data/test_file_duplicate_check_queue.py new file mode 100644 index 0000000..afa91c5 --- /dev/null +++ b/src/tests/ai/data/test_file_duplicate_check_queue.py @@ -0,0 +1,69 @@ +import pytest +import json +from unittest.mock import MagicMock, patch + +from src.main.ai.data.FileDuplicateCheckQueue import FileDuplicateCheckQueue + + +class TestFileDuplicateCheckQueue: + def setup_method(self): + # 목업 SQS 클라이언트 생성 + self.mock_sqs = MagicMock() + self.test_queue_url = "https://sqs.ap-northeast-2.amazonaws.com/123456789012/file-duplicate-check-queue.fifo" + + # 테스트 대상 큐 생성 + self.queue = FileDuplicateCheckQueue(self.mock_sqs, self.test_queue_url) + + # 테스트 공통 데이터 + self.test_request_id = "6123456789abcdef01234567" + self.test_user_id = "12345678-1234-5678-1234-567812345678" + self.test_s3_bucket = "test-bucket" + self.test_s3_key = "example.pdf" + + def test_send_message_success(self): + # given + expected_message_body = { + 'request_type': 'file_duplicate_check_embedding_file', + 'request_id': self.test_request_id, + 'user_id': self.test_user_id, + 'payload': { + 's3_bucket': self.test_s3_bucket, + 's3_key': self.test_s3_key + } + } + + expected_response = {"MessageId": "12345"} + self.mock_sqs.send_message.return_value = expected_response + + # when + response = self.queue.send_message( + request_id=self.test_request_id, + user_id=self.test_user_id, + s3_bucket=self.test_s3_bucket, + s3_key=self.test_s3_key + ) + + # then + self.mock_sqs.send_message.assert_called_once_with( + QueueUrl=self.test_queue_url, + MessageGroupId=self.test_user_id, + MessageDeduplicationId=self.test_request_id, + MessageBody=json.dumps(expected_message_body) + ) + assert response == expected_response + + def test_send_message_exception(self): + # given + self.mock_sqs.send_message.side_effect = Exception("SQS Error") + + # when & then + with pytest.raises(Exception) as exc_info: + self.queue.send_message( + request_id=self.test_request_id, + user_id=self.test_user_id, + s3_bucket=self.test_s3_bucket, + s3_key=self.test_s3_key + ) + + assert str(exc_info.value) == "SQS Error" + self.mock_sqs.send_message.assert_called_once() \ No newline at end of file diff --git a/src/tests/ai/data/test_file_duplicate_check_repository.py b/src/tests/ai/data/test_file_duplicate_check_repository.py new file mode 100644 index 0000000..16c457a --- /dev/null +++ b/src/tests/ai/data/test_file_duplicate_check_repository.py @@ -0,0 +1,253 @@ +import pytest +from unittest.mock import MagicMock, patch +from bson import ObjectId +from datetime import datetime, timezone + +from src.main.ai.data.FileDuplicateCheckRepository import FileDuplicateCheckRepository + + +class TestFileDuplicateCheckRepository: + def setup_method(self): + # 목업 MongoDB 클라이언트 생성 + self.mock_collection = MagicMock() + self.mock_files_collection = MagicMock() + self.mock_embeddings_collection = MagicMock() + self.mock_db = MagicMock() + + # 컬렉션 이름에 따라 적절한 mock 객체 반환 + def get_collection_side_effect(name): + if name == 'file_duplicate_checks': + return self.mock_collection + elif name == 'files': + return self.mock_files_collection + elif name == 'file_embeddings': + return self.mock_embeddings_collection + + self.mock_db.get_collection.side_effect = get_collection_side_effect + self.mock_client = MagicMock() + self.mock_client.get_database.return_value = self.mock_db + + # 테스트 대상 레포지토리 생성 + self.repository = FileDuplicateCheckRepository(self.mock_client) + + # 테스트 공통 데이터 + self.test_file_id = "6123456789abcdef01234567" + self.test_user_id = "12345678-1234-5678-1234-567812345678" + self.test_request_id = "7123456789abcdef01234567" + self.test_object_id = ObjectId(self.test_request_id) + self.test_file_object_id = ObjectId(self.test_file_id) + self.test_time = datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + + # 시간 고정을 위한 패치 + self.time_patch = patch.object(self.repository, 'get_current_time', return_value=self.test_time) + self.time_patch.start() + + def teardown_method(self): + self.time_patch.stop() + + def test_create_duplicate_check_request(self): + # given + expected_document = { + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": self.test_time + } + self.mock_collection.insert_one.return_value.inserted_id = self.test_object_id + + # mock find_one 결과 설정 + expected_result = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": self.test_time + } + self.mock_collection.find_one.return_value = expected_result + + # when + result = self.repository.create_duplicate_check_request(self.test_file_id, self.test_user_id) + + # then + self.mock_collection.insert_one.assert_called_once_with(expected_document) + assert result["_id"] == self.test_object_id + assert result["file_id"] == self.test_file_id + assert result["user_id"] == self.test_user_id + assert result["is_completed"] == False + assert result["is_duplicated"] is None + assert result["created_at"] == self.test_time + + def test_get_duplicate_check_by_id_found(self): + # given + expected_document = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": self.test_time + } + self.mock_collection.find_one.return_value = expected_document + + # when + result = self.repository.get_duplicate_check_by_id(self.test_request_id) + + # then + self.mock_collection.find_one.assert_called_once_with({"_id": self.test_object_id}) + assert result == expected_document + + def test_get_duplicate_check_by_id_not_found(self): + # given + self.mock_collection.find_one.return_value = None + + # when + result = self.repository.get_duplicate_check_by_id(self.test_request_id) + + # then + self.mock_collection.find_one.assert_called_once_with({"_id": self.test_object_id}) + assert result is None + + def test_get_duplicate_check_by_id_invalid_id(self): + # given + # ObjectId 변환 실패 + + # when + result = self.repository.get_duplicate_check_by_id("invalid_id") + + # then + self.mock_collection.find_one.assert_not_called() + assert result is None + + def test_get_duplicate_check_by_file_id_found(self): + # given + expected_document = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": self.test_time + } + self.mock_collection.find_one.return_value = expected_document + + # when + result = self.repository.get_duplicate_check_by_file_id(self.test_file_id, self.test_user_id) + + # then + self.mock_collection.find_one.assert_called_once_with({ + "file_id": self.test_file_id, + "user_id": self.test_user_id + }) + assert result == expected_document + + def test_get_duplicate_check_by_file_id_not_found(self): + # given + self.mock_collection.find_one.return_value = None + + # when + result = self.repository.get_duplicate_check_by_file_id(self.test_file_id, self.test_user_id) + + # then + self.mock_collection.find_one.assert_called_once_with({ + "file_id": self.test_file_id, + "user_id": self.test_user_id + }) + assert result is None + + def test_update_duplicate_check_result_success(self): + # given + # 업데이트 성공 + self.mock_collection.update_one.return_value.modified_count = 1 + + expected_document = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": True, + "is_duplicated": False, + "created_at": self.test_time, + "updated_at": self.test_time + } + self.mock_collection.find_one.return_value = expected_document + + # when + result = self.repository.update_duplicate_check_result(self.test_request_id, False) + + # then + self.mock_collection.update_one.assert_called_once_with( + {"_id": self.test_object_id}, + { + "$set": { + "is_completed": True, + "is_duplicated": False, + "updated_at": self.test_time + } + } + ) + self.mock_collection.find_one.assert_called_once_with({"_id": self.test_object_id}) + assert result == expected_document + + def test_update_duplicate_check_result_not_found(self): + # given + # 업데이트 실패 + self.mock_collection.update_one.return_value.modified_count = 0 + + # when + result = self.repository.update_duplicate_check_result(self.test_request_id, False) + + # then + self.mock_collection.update_one.assert_called_once() + self.mock_collection.find_one.assert_not_called() + assert result is None + + def test_update_duplicate_check_result_invalid_id(self): + # given + # ObjectId 변환 실패 + + # when + result = self.repository.update_duplicate_check_result("invalid_id", False) + + # then + self.mock_collection.update_one.assert_not_called() + self.mock_collection.find_one.assert_not_called() + assert result is None + + def test_get_file_by_id_found(self): + # given + expected_document = { + "_id": self.test_file_object_id, + "file_key": "example.pdf", + "s3_url": "https://bucket.s3.amazonaws.com/example.pdf" + } + self.mock_files_collection.find_one.return_value = expected_document + + # when + result = self.repository.get_file_by_id(self.test_file_id) + + # then + self.mock_files_collection.find_one.assert_called_once_with({"_id": self.test_file_object_id}) + assert result == expected_document + + def test_get_file_by_id_not_found(self): + # given + self.mock_files_collection.find_one.return_value = None + + # when + result = self.repository.get_file_by_id(self.test_file_id) + + # then + self.mock_files_collection.find_one.assert_called_once_with({"_id": self.test_file_object_id}) + assert result is None + + def test_get_file_by_id_invalid_id(self): + # given + # ObjectId 변환 실패 + + # when + result = self.repository.get_file_by_id("invalid_id") + + # then + self.mock_files_collection.find_one.assert_not_called() + assert result is None \ No newline at end of file diff --git a/src/tests/ai/models/__init__.py b/src/tests/ai/models/__init__.py index e69de29..b28b04f 100644 --- a/src/tests/ai/models/__init__.py +++ b/src/tests/ai/models/__init__.py @@ -0,0 +1,3 @@ + + + diff --git a/src/tests/ai/router/__init__.py b/src/tests/ai/router/__init__.py index e69de29..b28b04f 100644 --- a/src/tests/ai/router/__init__.py +++ b/src/tests/ai/router/__init__.py @@ -0,0 +1,3 @@ + + + diff --git a/src/tests/ai/router/test_ai_internal_api_router.py b/src/tests/ai/router/test_ai_internal_api_router.py index fd48baa..34a857e 100644 --- a/src/tests/ai/router/test_ai_internal_api_router.py +++ b/src/tests/ai/router/test_ai_internal_api_router.py @@ -1,9 +1,11 @@ import pytest from unittest.mock import MagicMock, patch -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient -from src.main.ai.models.CategoryRecommendation import CategoryRecommendationStatusResponse +from src.main.ai.models.CategoryRecommendation import CategoryRecommendationResultRequest +from src.main.ai.models.FileDuplicateCheck import FileDuplicateCheckResultRequest, FileDuplicateCheckRequest, FileDuplicateCheckResponse +from src.main.ai.service.FileDuplicateCheckService import FileDuplicateCheckService from src.main.ai.router.AIInternalAPIRouter import router as internal_router @@ -21,49 +23,180 @@ def setup_method(self): # 테스트 공통 데이터 self.test_request_id = "6123456789abcdef01234567" self.test_category = "기술" + self.test_user_id = "12345678-1234-5678-1234-567812345678" + self.test_file_id = "7123456789abcdef01234567" def test_update_category_recommendation_result_success(self, client): # given request_data = { + "request_id": self.test_request_id, "predicted_category": self.test_category } # 서비스 응답 모의 설정 with patch('src.main.ai.service.CategoryRecommendationService.CategoryRecommendationService.update_recommendation_result') as mock_service: # 서비스 응답 설정 - mock_service.return_value = CategoryRecommendationStatusResponse( - request_id=self.test_request_id, - is_completed=True, - predicted_category=self.test_category - ) + mock_service.return_value = True # when - response = client.post(f"/ai-proxy/category-recommendation-results/{self.test_request_id}", json=request_data) + response = client.post("/ai-proxy/category-recommendation-results", json=request_data) # then assert response.status_code == 200 - assert response.json() == { - "request_id": self.test_request_id, - "is_completed": True, - "predicted_category": self.test_category - } + assert response.json() == {"success": True} - # 서비스가 한 번 호출됐는지 확인 - mock_service.assert_called_once() + # 직접 호출 파라미터 체크 대신 호출 횟수만 확인 + assert mock_service.call_count == 1 def test_update_category_recommendation_result_not_found(self, client): # given request_data = { + "request_id": self.test_request_id, "predicted_category": self.test_category } - # 서비스 응답 모의 설정 - 업데이트 실패 + # 서비스 응답 모의 설정 with patch('src.main.ai.service.CategoryRecommendationService.CategoryRecommendationService.update_recommendation_result') as mock_service: + # 서비스 응답 설정 - 요청 없음 + mock_service.return_value = False + + # when + response = client.post("/ai-proxy/category-recommendation-results", json=request_data) + + # then + assert response.status_code == 404 + assert response.json() == { + "detail": "요청을 찾을 수 없습니다. 존재하지 않는 ID입니다." + } + + # 직접 호출 파라미터 체크 대신 호출 횟수만 확인 + assert mock_service.call_count == 1 + + def test_create_file_duplicate_check_success(self, client): + # given + request_data = { + "user_id": self.test_user_id, + "file_id": self.test_file_id + } + + # 서비스 응답 모의 설정 + with patch('src.main.ai.service.FileDuplicateCheckService.FileDuplicateCheckService.create_duplicate_check_request') as mock_service: # 서비스 응답 설정 - mock_service.return_value = None + mock_service.return_value = FileDuplicateCheckResponse(request_id=self.test_request_id) + + # when + response = client.post("/ai-proxy/file-duplicate-checks", json=request_data) + + # then + assert response.status_code == 200 + assert response.json() == {"request_id": self.test_request_id} + + mock_service.assert_called_once() + + def test_create_file_duplicate_check_file_not_found(self, client): + # given + request_data = { + "user_id": self.test_user_id, + "file_id": self.test_file_id + } + + # 서비스 응답 모의 설정 + with patch('src.main.ai.service.FileDuplicateCheckService.FileDuplicateCheckService.create_duplicate_check_request') as mock_service: + # 서비스 응답 설정 - 파일 없음 + mock_service.side_effect = HTTPException( + status_code=404, + detail="파일을 찾을 수 없습니다. 존재하지 않는 ID입니다." + ) + + # when + response = client.post("/ai-proxy/file-duplicate-checks", json=request_data) + + # then + assert response.status_code == 404 + assert response.json() == { + "detail": "파일을 찾을 수 없습니다. 존재하지 않는 ID입니다." + } + + mock_service.assert_called_once() + + def test_create_file_duplicate_check_existing_request(self, client): + # given + request_data = { + "user_id": self.test_user_id, + "file_id": self.test_file_id + } + + # 서비스 응답 모의 설정 + with patch('src.main.ai.service.FileDuplicateCheckService.FileDuplicateCheckService.create_duplicate_check_request') as mock_service: + # 서비스 응답 설정 - 이미 요청 존재함 + mock_service.side_effect = HTTPException( + status_code=400, + detail="이미 중복 검사 요청이 존재합니다." + ) + + # when + response = client.post("/ai-proxy/file-duplicate-checks", json=request_data) + + # then + assert response.status_code == 400 + assert response.json() == { + "detail": "이미 중복 검사 요청이 존재합니다." + } + + mock_service.assert_called_once() + + def test_update_file_duplicate_check_result_success(self, client): + # given + request_data = { + "request_id": self.test_request_id, + "is_duplicated": False + } + + # 서비스 응답 모의 설정 + with patch('src.main.ai.service.FileDuplicateCheckService.FileDuplicateCheckService.update_duplicate_check_result') as mock_service: + # 서비스 응답 설정 + mock_service.return_value = True + + # mock_repository.get_duplicate_check_by_id 설정 - 서비스가 아닌 서비스 인스턴스를 패치 + check_data = { + "_id": self.test_request_id, + "file_id": self.test_file_id, + "is_completed": True, + "is_duplicated": False + } + + # repository를 직접 패치하는 대신 get_duplicate_check_by_id 메소드만 패치 + with patch('src.main.ai.data.FileDuplicateCheckRepository.FileDuplicateCheckRepository.get_duplicate_check_by_id', + return_value=check_data) as mock_get_duplicate: + + # when + response = client.post("/ai-proxy/file-duplicate-check-embeddings", json=request_data) + + # then + assert response.status_code == 200 + assert response.json() == { + "request_id": self.test_request_id, + "file_id": self.test_file_id, + "is_completed": True, + "is_duplicated": False + } + + mock_service.assert_called_once_with(self.test_request_id, False) + + def test_update_file_duplicate_check_result_not_found(self, client): + # given + request_data = { + "request_id": self.test_request_id, + "is_duplicated": False + } + + # 서비스 응답 모의 설정 + with patch('src.main.ai.service.FileDuplicateCheckService.FileDuplicateCheckService.update_duplicate_check_result') as mock_service: + # 서비스 응답 설정 - 요청 없음 + mock_service.return_value = False # when - response = client.post(f"/ai-proxy/category-recommendation-results/{self.test_request_id}", json=request_data) + response = client.post("/ai-proxy/file-duplicate-check-embeddings", json=request_data) # then assert response.status_code == 404 @@ -73,5 +206,4 @@ def test_update_category_recommendation_result_not_found(self, client): "detail": "존재하지 않는 ID입니다." } - # 서비스가 한 번 호출됐는지 확인 - mock_service.assert_called_once() \ No newline at end of file + mock_service.assert_called_once_with(self.test_request_id, False) \ No newline at end of file diff --git a/src/tests/ai/router/test_ai_public_api_router.py b/src/tests/ai/router/test_ai_public_api_router.py index cc8e247..caff74c 100644 --- a/src/tests/ai/router/test_ai_public_api_router.py +++ b/src/tests/ai/router/test_ai_public_api_router.py @@ -1,7 +1,7 @@ import pytest import uuid from unittest.mock import MagicMock, patch -from fastapi import FastAPI, Depends +from fastapi import FastAPI, Depends, HTTPException from fastapi.testclient import TestClient from src.main.ai.models.CategoryRecommendation import ( @@ -9,6 +9,7 @@ CategoryRecommendationResponse, CategoryRecommendationStatusResponse ) +from src.main.ai.models.FileDuplicateCheck import FileDuplicateCheckStatusResponse from src.main.ai.router.AIPublicAPIRouter import router as public_router from src.main.auth.dependencies import get_current_user @@ -36,6 +37,7 @@ def setup_method(self): self.test_title = "테스트 제목" self.test_user_id = uuid.UUID("12345678-1234-5678-1234-567812345678") self.test_request_id = "6123456789abcdef01234567" + self.test_file_id = "7123456789abcdef01234567" def test_create_category_recommendation_request(self, client): # given @@ -53,15 +55,16 @@ def test_create_category_recommendation_request(self, client): # then assert response.status_code == 200 - assert response.json() == {"request_id": self.test_request_id} - - # 서비스가 한 번 호출됐는지 확인 + assert response.json() == { + "request_id": self.test_request_id + } mock_service.assert_called_once() def test_get_category_recommendation_status_exists(self, client): # given + # 서비스 응답 모의 설정 with patch('src.main.ai.service.CategoryRecommendationService.CategoryRecommendationService.get_recommendation_status') as mock_service: - # 서비스 응답 설정 - 완료된 추천 + # 서비스 응답 설정 mock_service.return_value = CategoryRecommendationStatusResponse( request_id=self.test_request_id, is_completed=True, @@ -78,14 +81,13 @@ def test_get_category_recommendation_status_exists(self, client): "is_completed": True, "predicted_category": "기술" } - - # 서비스가 한 번 호출됐는지 확인 mock_service.assert_called_once() def test_get_category_recommendation_status_not_found(self, client): # given + # 서비스 응답 모의 설정 with patch('src.main.ai.service.CategoryRecommendationService.CategoryRecommendationService.get_recommendation_status') as mock_service: - # 서비스 응답 설정 - 요청 없음 + # 서비스 응답 설정 mock_service.return_value = None # when @@ -93,4 +95,49 @@ def test_get_category_recommendation_status_not_found(self, client): # then assert response.status_code == 404 - assert "요청을 찾을 수 없습니다" in response.json().get("detail", "") \ No newline at end of file + assert response.json() == { + "detail": "요청을 찾을 수 없습니다. 존재하지 않는 ID입니다." + } + mock_service.assert_called_once() + + def test_get_file_duplicate_check_status_exists(self, client): + # given + # 서비스 응답 모의 설정 + with patch('src.main.ai.service.FileDuplicateCheckService.FileDuplicateCheckService.get_duplicate_check_status') as mock_service: + # 서비스 응답 설정 + mock_service.return_value = FileDuplicateCheckStatusResponse( + request_id=self.test_request_id, + file_id=self.test_file_id, + is_completed=True, + is_duplicated=False + ) + + # when + response = client.get(f"/ai/file-duplicate-checks?file_id={self.test_file_id}") + + # then + assert response.status_code == 200 + assert response.json() == { + "request_id": self.test_request_id, + "file_id": self.test_file_id, + "is_completed": True, + "is_duplicated": False + } + mock_service.assert_called_once_with(self.test_file_id, str(self.test_user_id)) + + def test_get_file_duplicate_check_status_not_found(self, client): + # given + # 서비스 응답 모의 설정 + with patch('src.main.ai.service.FileDuplicateCheckService.FileDuplicateCheckService.get_duplicate_check_status') as mock_service: + # 서비스 응답 설정 + mock_service.return_value = None + + # when + response = client.get(f"/ai/file-duplicate-checks?file_id={self.test_file_id}") + + # then + assert response.status_code == 404 + assert response.json() == { + "detail": "요청을 찾을 수 없습니다. 존재하지 않는 ID입니다." + } + mock_service.assert_called_once_with(self.test_file_id, str(self.test_user_id)) \ No newline at end of file diff --git a/src/tests/ai/service/__init__.py b/src/tests/ai/service/__init__.py index e69de29..b28b04f 100644 --- a/src/tests/ai/service/__init__.py +++ b/src/tests/ai/service/__init__.py @@ -0,0 +1,3 @@ + + + diff --git a/src/tests/ai/service/test_file_duplicate_check_service.py b/src/tests/ai/service/test_file_duplicate_check_service.py new file mode 100644 index 0000000..bd50acc --- /dev/null +++ b/src/tests/ai/service/test_file_duplicate_check_service.py @@ -0,0 +1,275 @@ +import pytest +import uuid +from unittest.mock import MagicMock, patch +from bson import ObjectId +from datetime import datetime, timezone +from fastapi import HTTPException + +from src.main.ai.service.FileDuplicateCheckService import FileDuplicateCheckService +from src.main.ai.models.FileDuplicateCheck import ( + FileDuplicateCheckRequest, + FileDuplicateCheckResponse, + FileDuplicateCheckStatusResponse, + FileDuplicateCheckResultRequest +) + + +class TestFileDuplicateCheckService: + def setup_method(self): + # 목업 리포지토리 및 큐 생성 + self.mock_repository = MagicMock() + self.mock_queue = MagicMock() + + # 테스트 대상 서비스 생성 + self.service = FileDuplicateCheckService(self.mock_repository, self.mock_queue) + + # 테스트 공통 데이터 + self.test_file_id = "6123456789abcdef01234567" + self.test_user_id = "12345678-1234-5678-1234-567812345678" + self.test_request_id = "7123456789abcdef01234567" + self.test_object_id = ObjectId(self.test_request_id) + self.test_file_object_id = ObjectId(self.test_file_id) + self.test_time = datetime(2023, 1, 1, tzinfo=timezone.utc) + + def test_create_duplicate_check_request_success(self): + # given + request = FileDuplicateCheckRequest( + user_id=self.test_user_id, + file_id=self.test_file_id + ) + + # 파일 존재함 + file_document = { + "_id": self.test_file_object_id, + "s3_bucket": "test-bucket", + "s3_key": "example.pdf", + "file_key": "example.pdf", + "s3_url": "https://bucket.s3.amazonaws.com/example.pdf" + } + self.mock_repository.get_file_by_id.return_value = file_document + + # 기존 요청 없음 + self.mock_repository.get_duplicate_check_by_file_id.return_value = None + + # 요청 생성 응답 + mongo_document = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": self.test_time + } + self.mock_repository.create_duplicate_check_request.return_value = mongo_document + + # when + result = self.service.create_duplicate_check_request(request) + + # then + self.mock_repository.get_file_by_id.assert_called_once_with(self.test_file_id) + self.mock_repository.get_duplicate_check_by_file_id.assert_called_once_with(self.test_file_id, self.test_user_id) + self.mock_repository.create_duplicate_check_request.assert_called_once_with( + file_id=self.test_file_id, + user_id=self.test_user_id + ) + self.mock_queue.send_message.assert_called_once() + + assert isinstance(result, FileDuplicateCheckResponse) + assert result.request_id == str(self.test_object_id) + + def test_create_duplicate_check_request_existing_request(self): + # given + request = FileDuplicateCheckRequest( + user_id=self.test_user_id, + file_id=self.test_file_id + ) + + # 파일 존재함 + file_document = { + "_id": self.test_file_object_id, + "s3_bucket": "test-bucket", + "s3_key": "example.pdf", + "file_key": "example.pdf", + "s3_url": "https://bucket.s3.amazonaws.com/example.pdf" + } + self.mock_repository.get_file_by_id.return_value = file_document + + # 이미 요청이 존재함 + existing_check = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": self.test_time + } + self.mock_repository.get_duplicate_check_by_file_id.return_value = existing_check + + # when & then + with pytest.raises(HTTPException) as exc_info: + self.service.create_duplicate_check_request(request) + + assert exc_info.value.status_code == 400 + assert exc_info.value.detail == "이미 중복 검사 요청이 존재합니다." + + self.mock_repository.get_file_by_id.assert_called_once_with(self.test_file_id) + self.mock_repository.get_duplicate_check_by_file_id.assert_called_once_with(self.test_file_id, self.test_user_id) + self.mock_repository.create_duplicate_check_request.assert_not_called() + self.mock_queue.send_message.assert_not_called() + + def test_create_duplicate_check_request_file_not_found(self): + # given + request = FileDuplicateCheckRequest( + user_id=self.test_user_id, + file_id=self.test_file_id + ) + + # 파일이 없음 + self.mock_repository.get_file_by_id.return_value = None + + # when & then + with pytest.raises(HTTPException) as exc_info: + self.service.create_duplicate_check_request(request) + + assert exc_info.value.status_code == 404 + self.mock_repository.get_file_by_id.assert_called_once_with(self.test_file_id) + self.mock_repository.get_duplicate_check_by_file_id.assert_not_called() + self.mock_repository.create_duplicate_check_request.assert_not_called() + self.mock_queue.send_message.assert_not_called() + + def test_get_duplicate_check_status_exists(self): + # given + # 리포지토리 응답 설정 - 완료되지 않은 중복 검사 + mongo_document = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": self.test_time + } + self.mock_repository.get_duplicate_check_by_file_id.return_value = mongo_document + + # when + result = self.service.get_duplicate_check_status(self.test_file_id, self.test_user_id) + + # then + self.mock_repository.get_duplicate_check_by_file_id.assert_called_once_with( + self.test_file_id, + self.test_user_id + ) + + assert isinstance(result, FileDuplicateCheckStatusResponse) + assert result.request_id == str(self.test_object_id) + assert result.file_id == self.test_file_id + assert result.is_completed == False + assert result.is_duplicated is None + + def test_get_duplicate_check_status_completed(self): + # given + # 리포지토리 응답 설정 - 완료된 중복 검사 + mongo_document = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": True, + "is_duplicated": False, + "created_at": self.test_time + } + self.mock_repository.get_duplicate_check_by_file_id.return_value = mongo_document + + # when + result = self.service.get_duplicate_check_status(self.test_file_id, self.test_user_id) + + # then + self.mock_repository.get_duplicate_check_by_file_id.assert_called_once_with( + self.test_file_id, + self.test_user_id + ) + + assert isinstance(result, FileDuplicateCheckStatusResponse) + assert result.request_id == str(self.test_object_id) + assert result.file_id == self.test_file_id + assert result.is_completed == True + assert result.is_duplicated == False + + def test_get_duplicate_check_status_not_found(self): + # given + # 리포지토리 응답 설정 - 문서 없음 + self.mock_repository.get_duplicate_check_by_file_id.return_value = None + + # when + result = self.service.get_duplicate_check_status(self.test_file_id, self.test_user_id) + + # then + self.mock_repository.get_duplicate_check_by_file_id.assert_called_once_with( + self.test_file_id, + self.test_user_id + ) + + assert result is None + + def test_update_duplicate_check_result_success(self): + # given + is_duplicated = False + + # 요청이 존재함 + check_document = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": False, + "is_duplicated": None, + "created_at": self.test_time + } + self.mock_repository.get_duplicate_check_by_id.return_value = check_document + + # 파일 중복 상태 업데이트 성공 + self.mock_repository.update_file_duplicate_status.return_value = { + "_id": self.test_file_object_id, + "s3_bucket": "test-bucket", + "s3_key": "example.pdf", + "is_duplicated": False + } + + # 업데이트 응답 + updated_document = { + "_id": self.test_object_id, + "file_id": self.test_file_id, + "user_id": self.test_user_id, + "is_completed": True, + "is_duplicated": False, + "created_at": self.test_time, + "updated_at": datetime(2023, 1, 2, tzinfo=timezone.utc) + } + self.mock_repository.update_duplicate_check_result.return_value = updated_document + + # when + result = self.service.update_duplicate_check_result(self.test_request_id, is_duplicated) + + # then + self.mock_repository.get_duplicate_check_by_id.assert_called_once_with(self.test_request_id) + self.mock_repository.update_file_duplicate_status.assert_called_once_with(self.test_file_id, is_duplicated) + self.mock_repository.update_duplicate_check_result.assert_called_once_with( + request_id=self.test_request_id, + is_duplicated=is_duplicated + ) + + assert result == True + + def test_update_duplicate_check_result_not_found(self): + # given + is_duplicated = False + + # 요청이 존재하지 않음 + self.mock_repository.get_duplicate_check_by_id.return_value = None + + # when + result = self.service.update_duplicate_check_result(self.test_request_id, is_duplicated) + + # then + self.mock_repository.get_duplicate_check_by_id.assert_called_once_with(self.test_request_id) + self.mock_repository.update_file_duplicate_status.assert_not_called() + self.mock_repository.update_duplicate_check_result.assert_not_called() + + assert result == False \ No newline at end of file