diff --git a/app.py b/app.py index 87f9391..07e495b 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,4 @@ - - -from app.factory import create_app +from hexactf.factory import create_app app = create_app() diff --git a/app/__pycache__/__init__.cpython-310.pyc b/app/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 6b7d380..0000000 Binary files a/app/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/app/__pycache__/__init__.cpython-39.pyc b/app/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 2a3b394..0000000 Binary files a/app/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/app/__pycache__/factory.cpython-310.pyc b/app/__pycache__/factory.cpython-310.pyc deleted file mode 100644 index a19f2d6..0000000 Binary files a/app/__pycache__/factory.cpython-310.pyc and /dev/null differ diff --git a/app/__pycache__/factory.cpython-39.pyc b/app/__pycache__/factory.cpython-39.pyc deleted file mode 100644 index 6a79da7..0000000 Binary files a/app/__pycache__/factory.cpython-39.pyc and /dev/null differ diff --git a/app/extensions/db/repository_test.py b/app/extensions/db/repository_test.py deleted file mode 100644 index 9a47d30..0000000 --- a/app/extensions/db/repository_test.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest -from unittest.mock import MagicMock -from app.repositories.user_challenges_repository import UserChallengesRepository -from app.extensions.db.models import UserChallenges - -@pytest.fixture -def mock_session(): - return MagicMock() - -@pytest.fixture -def user_challenges_repo(mock_session): - return UserChallengesRepository(session=mock_session) - -@pytest.fixture -def mock_challenge(): - return UserChallenges(C_idx=1, username="test_user", status="Running", port=8080) - -def test_get_status_running(user_challenges_repo, mock_session, mock_challenge): - mock_session.query.return_value.filter_by.return_value.first.return_value = mock_challenge - result = user_challenges_repo.get_status(challenge_id=1, username="test_user") - assert result == {'status': 'Running', 'port': 8080} - -def test_get_status_not_running(user_challenges_repo, mock_session): - mock_challenge = UserChallenges(C_idx=1, username="test_user", status="Stopped", port=8080) - mock_session.query.return_value.filter_by.return_value.first.return_value = mock_challenge - result = user_challenges_repo.get_status(challenge_id=1, username="test_user") - assert result == {'status': 'Stopped'} - -def test_get_status_not_found(user_challenges_repo, mock_session): - mock_session.query.return_value.filter_by.return_value.first.return_value = None - result = user_challenges_repo.get_status(challenge_id=1, username="nonexistent_user") - assert result is None diff --git a/app/extensions/kafka/__init__.py b/app/extensions/kafka/__init__.py deleted file mode 100644 index dccd3af..0000000 --- a/app/extensions/kafka/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -__version__ = '1.0.0' - -__all__ = [ - 'KafkaEventConsumer', - 'KafkaConfig', -] - -from app.extensions.kafka.config import KafkaConfig -from app.extensions.kafka.consumer import KafkaEventConsumer diff --git a/app/monitoring/__pycache__/__init__.cpython-310.pyc b/app/monitoring/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 2598f91..0000000 Binary files a/app/monitoring/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/app/monitoring/__pycache__/__init__.cpython-39.pyc b/app/monitoring/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 8f77c97..0000000 Binary files a/app/monitoring/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/app/monitoring/__pycache__/async_handler.cpython-39.pyc b/app/monitoring/__pycache__/async_handler.cpython-39.pyc deleted file mode 100644 index 60e9b76..0000000 Binary files a/app/monitoring/__pycache__/async_handler.cpython-39.pyc and /dev/null differ diff --git a/app/monitoring/__pycache__/ctf_metrics_collector.cpython-39.pyc b/app/monitoring/__pycache__/ctf_metrics_collector.cpython-39.pyc deleted file mode 100644 index 8f92e9f..0000000 Binary files a/app/monitoring/__pycache__/ctf_metrics_collector.cpython-39.pyc and /dev/null differ diff --git a/app/monitoring/__pycache__/loki_logger.cpython-39.pyc b/app/monitoring/__pycache__/loki_logger.cpython-39.pyc deleted file mode 100644 index bd26880..0000000 Binary files a/app/monitoring/__pycache__/loki_logger.cpython-39.pyc and /dev/null differ diff --git a/app/__init__.py b/hexactf/__init__.py similarity index 100% rename from app/__init__.py rename to hexactf/__init__.py diff --git a/hexactf/__pycache__/__init__.cpython-310.pyc b/hexactf/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..b6c39ee Binary files /dev/null and b/hexactf/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/__pycache__/config.cpython-310.pyc b/hexactf/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..8e154e6 Binary files /dev/null and b/hexactf/__pycache__/config.cpython-310.pyc differ diff --git a/hexactf/__pycache__/extensions_manager.cpython-310.pyc b/hexactf/__pycache__/extensions_manager.cpython-310.pyc new file mode 100644 index 0000000..5da603f Binary files /dev/null and b/hexactf/__pycache__/extensions_manager.cpython-310.pyc differ diff --git a/hexactf/__pycache__/factory.cpython-310.pyc b/hexactf/__pycache__/factory.cpython-310.pyc new file mode 100644 index 0000000..f493417 Binary files /dev/null and b/hexactf/__pycache__/factory.cpython-310.pyc differ diff --git a/app/api/__init__.py b/hexactf/api/__init__.py similarity index 100% rename from app/api/__init__.py rename to hexactf/api/__init__.py diff --git a/hexactf/api/__pycache__/__init__.cpython-310.pyc b/hexactf/api/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..7f4f927 Binary files /dev/null and b/hexactf/api/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/api/__pycache__/challenge_api.cpython-310.pyc b/hexactf/api/__pycache__/challenge_api.cpython-310.pyc new file mode 100644 index 0000000..5a5b357 Binary files /dev/null and b/hexactf/api/__pycache__/challenge_api.cpython-310.pyc differ diff --git a/app/api/challenge.py b/hexactf/api/challenge_api.py similarity index 87% rename from app/api/challenge.py rename to hexactf/api/challenge_api.py index 0139a4b..65cd30e 100644 --- a/app/api/challenge.py +++ b/hexactf/api/challenge_api.py @@ -1,14 +1,11 @@ from json import JSONDecodeError from logging import log -import sys -from app.monitoring.ctf_metrics_collector import ChallengeMetricsCollector from flask import Blueprint, jsonify, request -from app.exceptions.api import InvalidRequest -from app.exceptions.userchallenge import UserChallengeCreationError, UserChallengeDeletionError, UserChallengeNotFoundError -from app.extensions.db.repository import UserChallengesRepository -from app.extensions.k8s.client import K8sClient -from app.monitoring.ctf_metrics_collector import challenge_metrics_collector +from hexactf.exceptions.api_exceptions import InvalidRequest +from hexactf.exceptions.userchallenge_exceptions import UserChallengeCreationError, UserChallengeDeletionError, UserChallengeNotFoundError +from hexactf.extensions.db.repository import UserChallengesRepository +from hexactf.extensions.k8s.client import K8sClient challenge_bp = Blueprint('challenge', __name__) @@ -69,7 +66,6 @@ def get_userchallenge_status(): try: # Challenge 관련 정보 가져오기 res = request.get_json() - # print(f"/status : {res}",file=sys.stderr) if not res: raise InvalidRequest(error_msg="Request body is empty or not valid JSON") diff --git a/app/config.py b/hexactf/config.py similarity index 100% rename from app/config.py rename to hexactf/config.py diff --git a/app/exceptions/__init__.py b/hexactf/exceptions/__init__.py similarity index 100% rename from app/exceptions/__init__.py rename to hexactf/exceptions/__init__.py diff --git a/hexactf/exceptions/__pycache__/__init__.cpython-310.pyc b/hexactf/exceptions/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..a2667fa Binary files /dev/null and b/hexactf/exceptions/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/api_exceptions.cpython-310.pyc b/hexactf/exceptions/__pycache__/api_exceptions.cpython-310.pyc new file mode 100644 index 0000000..5d33390 Binary files /dev/null and b/hexactf/exceptions/__pycache__/api_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/base_exceptions.cpython-310.pyc b/hexactf/exceptions/__pycache__/base_exceptions.cpython-310.pyc new file mode 100644 index 0000000..3ce3945 Binary files /dev/null and b/hexactf/exceptions/__pycache__/base_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc b/hexactf/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc new file mode 100644 index 0000000..286fe0b Binary files /dev/null and b/hexactf/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/error_types.cpython-310.pyc b/hexactf/exceptions/__pycache__/error_types.cpython-310.pyc new file mode 100644 index 0000000..2cd78f3 Binary files /dev/null and b/hexactf/exceptions/__pycache__/error_types.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc b/hexactf/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc new file mode 100644 index 0000000..f986c6f Binary files /dev/null and b/hexactf/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc b/hexactf/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc new file mode 100644 index 0000000..97d7ab7 Binary files /dev/null and b/hexactf/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc differ diff --git a/app/exceptions/api.py b/hexactf/exceptions/api_exceptions.py similarity index 88% rename from app/exceptions/api.py rename to hexactf/exceptions/api_exceptions.py index 48b49c3..8fb667d 100644 --- a/app/exceptions/api.py +++ b/hexactf/exceptions/api_exceptions.py @@ -1,6 +1,6 @@ -from app.exceptions.base import CustomBaseException -from app.exceptions.error_types import ApiErrorTypes +from hexactf.exceptions.base_exceptions import CustomBaseException +from hexactf.exceptions.error_types import ApiErrorTypes class APIException(CustomBaseException): diff --git a/app/exceptions/base.py b/hexactf/exceptions/base_exceptions.py similarity index 94% rename from app/exceptions/base.py rename to hexactf/exceptions/base_exceptions.py index a8c20db..c0d2229 100644 --- a/app/exceptions/base.py +++ b/hexactf/exceptions/base_exceptions.py @@ -1,6 +1,6 @@ # app/utils/exceptions.py -from app.exceptions.error_types import ApiErrorTypes +from hexactf.exceptions.error_types import ApiErrorTypes class CustomBaseException(Exception): """ diff --git a/app/exceptions/challenge.py b/hexactf/exceptions/challenge_exceptions.py similarity index 83% rename from app/exceptions/challenge.py rename to hexactf/exceptions/challenge_exceptions.py index cd6cd45..427982e 100644 --- a/app/exceptions/challenge.py +++ b/hexactf/exceptions/challenge_exceptions.py @@ -1,5 +1,5 @@ -from app.exceptions.base import CustomBaseException -from app.exceptions.error_types import ApiErrorTypes +from hexactf.exceptions.base_exceptions import CustomBaseException +from hexactf.exceptions.error_types import ApiErrorTypes class ChallengeException(CustomBaseException): diff --git a/app/exceptions/error_types.py b/hexactf/exceptions/error_types.py similarity index 100% rename from app/exceptions/error_types.py rename to hexactf/exceptions/error_types.py diff --git a/app/exceptions/handlers.py b/hexactf/exceptions/handlers.py similarity index 86% rename from app/exceptions/handlers.py rename to hexactf/exceptions/handlers.py index 59944cb..d42670b 100644 --- a/app/exceptions/handlers.py +++ b/hexactf/exceptions/handlers.py @@ -1,6 +1,6 @@ from flask import jsonify -from app.exceptions.base import CustomBaseException +from hexactf.exceptions.base_exceptions import CustomBaseException def register_error_handler(app): """CustomBaseException을 처리하는 에러 핸들러""" diff --git a/app/exceptions/kafka.py b/hexactf/exceptions/kafka_exceptions.py similarity index 93% rename from app/exceptions/kafka.py rename to hexactf/exceptions/kafka_exceptions.py index 868907a..fb0bc7b 100644 --- a/app/exceptions/kafka.py +++ b/hexactf/exceptions/kafka_exceptions.py @@ -1,5 +1,5 @@ -from app.exceptions.base import CustomBaseException -from app.exceptions.error_types import ApiErrorTypes +from hexactf.exceptions.base_exceptions import CustomBaseException +from hexactf.exceptions.error_types import ApiErrorTypes class QueueException(CustomBaseException): """Queue(Kafka) 관련 기본 예외""" diff --git a/app/exceptions/userchallenge.py b/hexactf/exceptions/userchallenge_exceptions.py similarity index 93% rename from app/exceptions/userchallenge.py rename to hexactf/exceptions/userchallenge_exceptions.py index fad8544..83da20e 100644 --- a/app/exceptions/userchallenge.py +++ b/hexactf/exceptions/userchallenge_exceptions.py @@ -1,6 +1,6 @@ -from app.exceptions.base import CustomBaseException -from app.exceptions.error_types import ApiErrorTypes +from hexactf.exceptions.base_exceptions import CustomBaseException +from hexactf.exceptions.error_types import ApiErrorTypes class UserChallengeException(CustomBaseException): """UserChallenge 관련 기본 예외""" diff --git a/app/extensions/__init__.py b/hexactf/extensions/__init__.py similarity index 100% rename from app/extensions/__init__.py rename to hexactf/extensions/__init__.py diff --git a/hexactf/extensions/__pycache__/__init__.cpython-310.pyc b/hexactf/extensions/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..7046e17 Binary files /dev/null and b/hexactf/extensions/__pycache__/__init__.cpython-310.pyc differ diff --git a/app/extensions/db/__init__.py b/hexactf/extensions/db/__init__.py similarity index 62% rename from app/extensions/db/__init__.py rename to hexactf/extensions/db/__init__.py index 2457388..db783c9 100644 --- a/app/extensions/db/__init__.py +++ b/hexactf/extensions/db/__init__.py @@ -3,4 +3,4 @@ __all__ = ['MariaDBConfig'] from flask_sqlalchemy import SQLAlchemy -from app.extensions.db.config import MariaDBConfig +from hexactf.extensions.db.config import MariaDBConfig diff --git a/hexactf/extensions/db/__pycache__/__init__.cpython-310.pyc b/hexactf/extensions/db/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..674d459 Binary files /dev/null and b/hexactf/extensions/db/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/extensions/db/__pycache__/config.cpython-310.pyc b/hexactf/extensions/db/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..69fdfb7 Binary files /dev/null and b/hexactf/extensions/db/__pycache__/config.cpython-310.pyc differ diff --git a/hexactf/extensions/db/__pycache__/models.cpython-310.pyc b/hexactf/extensions/db/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..07f2619 Binary files /dev/null and b/hexactf/extensions/db/__pycache__/models.cpython-310.pyc differ diff --git a/hexactf/extensions/db/__pycache__/repository.cpython-310.pyc b/hexactf/extensions/db/__pycache__/repository.cpython-310.pyc new file mode 100644 index 0000000..b8e8632 Binary files /dev/null and b/hexactf/extensions/db/__pycache__/repository.cpython-310.pyc differ diff --git a/app/extensions/db/config.py b/hexactf/extensions/db/config.py similarity index 100% rename from app/extensions/db/config.py rename to hexactf/extensions/db/config.py diff --git a/app/extensions/db/models.py b/hexactf/extensions/db/models.py similarity index 99% rename from app/extensions/db/models.py rename to hexactf/extensions/db/models.py index f29b18a..235e482 100644 --- a/app/extensions/db/models.py +++ b/hexactf/extensions/db/models.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from app.extensions_manager import db +from hexactf.extensions_manager import db from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship diff --git a/app/extensions/db/repository.py b/hexactf/extensions/db/repository.py similarity index 84% rename from app/extensions/db/repository.py rename to hexactf/extensions/db/repository.py index fc48a0a..2fa9bb2 100644 --- a/app/extensions/db/repository.py +++ b/hexactf/extensions/db/repository.py @@ -1,13 +1,10 @@ import logging -from sqlite3 import OperationalError -from typing import List, Optional +from typing import Optional from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session, load_only from sqlalchemy.sql import text -from contextlib import contextmanager -from app.exceptions.api import InternalServerError -from app.extensions_manager import db -from app.extensions.db.models import Challenges, UserChallenges +from hexactf.exceptions.api_exceptions import InternalServerError +from hexactf.extensions_manager import db +from hexactf.extensions.db.models import Challenges, UserChallenges class UserChallengesRepository: def __init__(self, session=None): @@ -76,13 +73,9 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: fresh_challenge = self.session.merge(challenge) self.session.refresh(fresh_challenge) fresh_challenge.status = new_status - # self.session.add(challenge) # Add this line to track the object - # self.session.flush() self.session.commit() return True except SQLAlchemyError as e: - # logger.error(f"Error updating challenge status: {e}") - self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e @@ -139,16 +132,6 @@ def get_status(self, challenge_id, username) -> Optional[dict]: return {'status': challenge.status, 'port': int(challenge.port)} return {'status': challenge.status} -# class ChallengeRepository: -# def __init__(self): -# self.db_session = db.session - -# def get_challenge_name(self, challenge_id: int) -> Optional[str]: -# """챌린지 ID로 챌린지 조회""" -# with self.get_session() as session: -# challenge = session.query(Challenges).get(challenge_id) -# return challenge.title if challenge else None - class ChallengeRepository: @staticmethod diff --git a/app/extensions/k8s/__init__.py b/hexactf/extensions/k8s/__init__.py similarity index 100% rename from app/extensions/k8s/__init__.py rename to hexactf/extensions/k8s/__init__.py diff --git a/hexactf/extensions/k8s/__pycache__/__init__.cpython-310.pyc b/hexactf/extensions/k8s/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..804a337 Binary files /dev/null and b/hexactf/extensions/k8s/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/extensions/k8s/__pycache__/client.cpython-310.pyc b/hexactf/extensions/k8s/__pycache__/client.cpython-310.pyc new file mode 100644 index 0000000..4b80942 Binary files /dev/null and b/hexactf/extensions/k8s/__pycache__/client.cpython-310.pyc differ diff --git a/app/extensions/k8s/client.py b/hexactf/extensions/k8s/client.py similarity index 93% rename from app/extensions/k8s/client.py rename to hexactf/extensions/k8s/client.py index 0eeed02..59e189c 100644 --- a/app/extensions/k8s/client.py +++ b/hexactf/extensions/k8s/client.py @@ -4,10 +4,9 @@ from kubernetes import client, config -from app.exceptions.challenge import ChallengeNotFound -from app.exceptions.userchallenge import UserChallengeCreationError, UserChallengeDeletionError -from app.extensions.db.repository import ChallengeRepository, UserChallengesRepository -from app.monitoring.loki_logger import FlaskLokiLogger +from hexactf.exceptions.challenge_exceptions import ChallengeNotFound +from hexactf.exceptions.userchallenge_exceptions import UserChallengeCreationError, UserChallengeDeletionError +from hexactf.extensions.db.repository import ChallengeRepository, UserChallengesRepository MAX_RETRIES = 3 @@ -148,19 +147,12 @@ def _normalize_k8s_name(self, name: str) -> str: if not name or len(name) > 253: raise ValueError("이름이 비어있거나 길이가 253자를 초과함") - # 1. 소문자로 변환 name = name.lower() - - # 2. 공백 및 비허용 문자 (`[^a-z0-9-]`)를 `-`로 변환 name = re.sub(r'[^a-z0-9-]+', '-', name) - - # 3. 하이픈(-)이 연속적으로 나오면 하나로 줄이기 name = re.sub(r'-+', '-', name) - - # 4. 앞뒤의 하이픈 제거 name = name.strip('-') - # 5. 최종 길이 검사 (1~253자) + # 최종 길이 검사 (1~253자) if not name or len(name) > 253: raise ValueError(f"변환 후에도 유효하지 않은 Kubernetes 리소스 이름: {name}") diff --git a/hexactf/extensions/kafka/__init__.py b/hexactf/extensions/kafka/__init__.py new file mode 100644 index 0000000..04c5eca --- /dev/null +++ b/hexactf/extensions/kafka/__init__.py @@ -0,0 +1,9 @@ +__version__ = '1.0.0' + +__all__ = [ + 'KafkaEventConsumer', + 'KafkaConfig', +] + +from hexactf.extensions.kafka.config import KafkaConfig +from hexactf.extensions.kafka.consumer import KafkaEventConsumer diff --git a/hexactf/extensions/kafka/__pycache__/__init__.cpython-310.pyc b/hexactf/extensions/kafka/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..7f77c3e Binary files /dev/null and b/hexactf/extensions/kafka/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/extensions/kafka/__pycache__/config.cpython-310.pyc b/hexactf/extensions/kafka/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..ae7b145 Binary files /dev/null and b/hexactf/extensions/kafka/__pycache__/config.cpython-310.pyc differ diff --git a/hexactf/extensions/kafka/__pycache__/consumer.cpython-310.pyc b/hexactf/extensions/kafka/__pycache__/consumer.cpython-310.pyc new file mode 100644 index 0000000..1353c0a Binary files /dev/null and b/hexactf/extensions/kafka/__pycache__/consumer.cpython-310.pyc differ diff --git a/hexactf/extensions/kafka/__pycache__/handler.cpython-310.pyc b/hexactf/extensions/kafka/__pycache__/handler.cpython-310.pyc new file mode 100644 index 0000000..ace2d47 Binary files /dev/null and b/hexactf/extensions/kafka/__pycache__/handler.cpython-310.pyc differ diff --git a/app/extensions/kafka/config.py b/hexactf/extensions/kafka/config.py similarity index 100% rename from app/extensions/kafka/config.py rename to hexactf/extensions/kafka/config.py diff --git a/app/extensions/kafka/consumer.py b/hexactf/extensions/kafka/consumer.py similarity index 98% rename from app/extensions/kafka/consumer.py rename to hexactf/extensions/kafka/consumer.py index 2aa288d..bbb8171 100644 --- a/app/extensions/kafka/consumer.py +++ b/hexactf/extensions/kafka/consumer.py @@ -3,7 +3,7 @@ from typing import Any, Dict from kafka import KafkaConsumer import json -from app.exceptions.kafka import QueueProcessingError +from hexactf.exceptions.kafka_exceptions import QueueProcessingError class StatusMessage: diff --git a/app/extensions/kafka/handler.py b/hexactf/extensions/kafka/handler.py similarity index 95% rename from app/extensions/kafka/handler.py rename to hexactf/extensions/kafka/handler.py index bb5fcd0..5d045b5 100644 --- a/app/extensions/kafka/handler.py +++ b/hexactf/extensions/kafka/handler.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict -from app.exceptions.kafka import QueueProcessingError -from app.extensions.db.repository import UserChallengesRepository +from hexactf.exceptions.kafka_exceptions import QueueProcessingError +from hexactf.extensions.db.repository import UserChallengesRepository logger = logging.getLogger(__name__) diff --git a/app/extensions_manager.py b/hexactf/extensions_manager.py similarity index 98% rename from app/extensions_manager.py rename to hexactf/extensions_manager.py index a07c0d1..07eee54 100644 --- a/app/extensions_manager.py +++ b/hexactf/extensions_manager.py @@ -4,7 +4,7 @@ from typing import Optional, Callable from flask import Flask from flask_sqlalchemy import SQLAlchemy -from app.extensions.kafka import KafkaConfig, KafkaEventConsumer +from hexactf.extensions.kafka import KafkaConfig, KafkaEventConsumer class FlaskKafkaConsumer: """Flask 애플리케이션에서 Kafka 메시지 소비를 관리하는 클래스""" diff --git a/app/factory.py b/hexactf/factory.py similarity index 85% rename from app/factory.py rename to hexactf/factory.py index 884a579..7e8b7c8 100644 --- a/app/factory.py +++ b/hexactf/factory.py @@ -1,21 +1,16 @@ +import os import sys -from requests import Response -from app.monitoring.ctf_metrics_collector import ChallengeMetricsCollector -from app.monitoring.loki_logger import FlaskLokiLogger -from app.monitoring.system_metrics_collector import SystemMetricsCollector +# from hexactf.monitoring.system_metrics_collector import SystemMetricsCollector from flask import Flask, g, request -import threading from datetime import datetime from typing import Any, Dict, Type -from prometheus_client import REGISTRY, generate_latest, CONTENT_TYPE_LATEST -from app.api.challenge import challenge_bp -from app.config import Config -from app.exceptions.base import CustomBaseException -from app.exceptions.handlers import register_error_handler -from app.extensions.kafka.handler import MessageHandler -from app.extensions_manager import kafka_consumer, db +from hexactf.api.challenge_api import challenge_bp +from hexactf.config import Config +from hexactf.exceptions.base_exceptions import CustomBaseException +from hexactf.extensions.kafka.handler import MessageHandler +from hexactf.extensions_manager import kafka_consumer, db def start_kafka_consumer(app): """Start Kafka consumer in a separate thread""" @@ -26,15 +21,22 @@ class FlaskApp: def __init__(self, config_class: Type[Config] = Config): self.app = Flask(__name__) self.app.config.from_object(config_class) + self.logger = FlaskLokiLogger(app_name="challenge-api", loki_url=self.app.config['LOKI_URL']).logger + + if os.getenv("TEST_MODE") != "true": + from hexactf.monitoring.loki_logger import FlaskLokiLogger + self.logger = FlaskLokiLogger(app_name="challenge-api", loki_url=self.app.config['LOKI_URL']).logger + else: + self.logger = self.app.logger # Use Flask default logger # 초기 설정 self._init_extensions() self._setup_middleware() self._register_error_handlers() self._setup_blueprints() - self._init_metrics_collector() - + # self._init_metrics_collector() + def _init_extensions(self): """Extensions 초기화""" # Kafka 초기화 @@ -46,11 +48,12 @@ def _init_extensions(self): with self.app.app_context(): db.create_all() - def _init_metrics_collector(self): + # @NOTE : 추후 제거 예정 + # def _init_metrics_collector(self): - # System 메트릭 수집기 초기화 - system_collector = SystemMetricsCollector(self.app) - system_collector.start_collecting() + # # System 메트릭 수집기 초기화 + # system_collector = SystemMetricsCollector(self.app) + # system_collector.start_collecting() def _setup_middleware(self): diff --git a/app/monitoring/__init__.py b/hexactf/monitoring/__init__.py similarity index 100% rename from app/monitoring/__init__.py rename to hexactf/monitoring/__init__.py diff --git a/hexactf/monitoring/__pycache__/__init__.cpython-310.pyc b/hexactf/monitoring/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..b714999 Binary files /dev/null and b/hexactf/monitoring/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/monitoring/__pycache__/async_handler.cpython-310.pyc b/hexactf/monitoring/__pycache__/async_handler.cpython-310.pyc new file mode 100644 index 0000000..dbe3f71 Binary files /dev/null and b/hexactf/monitoring/__pycache__/async_handler.cpython-310.pyc differ diff --git a/app/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc b/hexactf/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc similarity index 90% rename from app/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc rename to hexactf/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc index 7c078cd..b1c37d5 100644 Binary files a/app/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc and b/hexactf/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc differ diff --git a/hexactf/monitoring/__pycache__/loki_logger.cpython-310.pyc b/hexactf/monitoring/__pycache__/loki_logger.cpython-310.pyc new file mode 100644 index 0000000..f039ded Binary files /dev/null and b/hexactf/monitoring/__pycache__/loki_logger.cpython-310.pyc differ diff --git a/hexactf/monitoring/__pycache__/system_metrics_collector.cpython-310.pyc b/hexactf/monitoring/__pycache__/system_metrics_collector.cpython-310.pyc new file mode 100644 index 0000000..999cdf4 Binary files /dev/null and b/hexactf/monitoring/__pycache__/system_metrics_collector.cpython-310.pyc differ diff --git a/app/monitoring/async_handler.py b/hexactf/monitoring/async_handler.py similarity index 100% rename from app/monitoring/async_handler.py rename to hexactf/monitoring/async_handler.py diff --git a/app/monitoring/ctf_metrics_collector.py b/hexactf/monitoring/ctf_metrics_collector.py similarity index 100% rename from app/monitoring/ctf_metrics_collector.py rename to hexactf/monitoring/ctf_metrics_collector.py diff --git a/app/monitoring/loki_logger.py b/hexactf/monitoring/loki_logger.py similarity index 98% rename from app/monitoring/loki_logger.py rename to hexactf/monitoring/loki_logger.py index 55b77c0..43a7b81 100644 --- a/app/monitoring/loki_logger.py +++ b/hexactf/monitoring/loki_logger.py @@ -1,14 +1,11 @@ import time import json import logging -import sys import traceback -from app.monitoring.async_handler import AsyncHandler +from hexactf.monitoring.async_handler import AsyncHandler from logging_loki import LokiHandler - - class FlaskLokiLogger: def __init__(self, app_name,loki_url: str): self.app_name = app_name diff --git a/app/monitoring/system_metrics_collector.py b/hexactf/monitoring/system_metrics_collector.py similarity index 100% rename from app/monitoring/system_metrics_collector.py rename to hexactf/monitoring/system_metrics_collector.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . diff --git a/requirements.txt b/requirements.txt index f9370c2..178bd34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,8 @@ SQLAlchemy==2.0.20 pymysql==1.0.3 kubernetes==26.1.0 Flask-SQLAlchemy==3.0.2 -mariadb>=1.0.11 +# mariadb>=1.0.11 prometheus-client==0.19.0 python-logging-loki==0.3.1 flask-prometheus-metrics==1.0.0 -psutil==5.9.8 \ No newline at end of file +psutil \ No newline at end of file diff --git a/tests/units/.gitignore b/tests/units/.gitignore new file mode 100644 index 0000000..d9cf955 --- /dev/null +++ b/tests/units/.gitignore @@ -0,0 +1,2 @@ +.pytest_cache +__pycache__/ diff --git a/tests/units/__init__.py b/tests/units/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/units/db/__init__.py b/tests/units/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/units/db/conftest.py b/tests/units/db/conftest.py new file mode 100644 index 0000000..ca4a675 --- /dev/null +++ b/tests/units/db/conftest.py @@ -0,0 +1,48 @@ +import os +from flask import Flask +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session + +from hexactf.extensions.db.models import UserChallenges +from hexactf.extensions_manager import db + +class TestDB: + """Test database setup for integration testing""" + def __init__(self): + self.app = Flask(__name__) + self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:' + self.app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + self.engine = create_engine(self.app.config['SQLALCHEMY_DATABASE_URI']) + self.session_factory = sessionmaker(bind=self.engine) + self.Session = scoped_session(self.session_factory) + + with self.app.app_context(): + db.init_app(self.app) + db.create_all() + db.session.commit() + + def get_session(self): + with self.app.app_context(): + return db.session + + def close(self): + with self.app.app_context(): + db.session.remove() + db.drop_all() + +@pytest.fixture() +def test_db(): + """Provides an instance of TestDB for database tests inside a Flask app context""" + test_db_instance = TestDB() + with test_db_instance.app.app_context(): + yield test_db_instance + test_db_instance.close() + +# autouse 명시적으로 call 하지 않아도 사용할 수 있는 옵션 +@pytest.fixture(scope="session", autouse=True) +def set_test_env(): + """Automatically set TEST_MODE=true for all tests""" + os.environ["TEST_MODE"] = "true" + \ No newline at end of file diff --git a/tests/units/db/test_userchallenges_repo.py b/tests/units/db/test_userchallenges_repo.py new file mode 100644 index 0000000..a571b90 --- /dev/null +++ b/tests/units/db/test_userchallenges_repo.py @@ -0,0 +1,173 @@ +import pytest +from hexactf.exceptions.api_exceptions import InternalServerError +from hexactf.extensions.db.models import UserChallenges +from hexactf.extensions.db.repository import UserChallengesRepository + +# ============================ +# create_challenge +# ============================ + +def test_create_challenge_success(test_db): + """Test successful challenge creation""" + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + challenge = user_challenges_repo.create( + username="test_user", + C_idx=1, + userChallengeName="test_challenge", + port=30000 + ) + + assert challenge is not None + assert challenge.username == "test_user" + assert challenge.C_idx == 1 + assert challenge.userChallengeName == "test_challenge" + assert challenge.port == 30000 + +def test_create_challenge_None_value_failed(test_db): + """Test challenge creation failure due to database error""" + + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + with pytest.raises(InternalServerError): + user_challenges_repo.create( + username=None, + C_idx=1, + userChallengeName="test_challenge", + port=30000 + ) + +# ============================ +# get_by_user_challenge_name +# ============================ + +def test_get_by_user_challenge_name_success(test_db): + """Test retrieving a challenge by name""" + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + challenge = UserChallenges( + username="test_user", + C_idx=1, + userChallengeName="test_challenge", + port=30000 + ) + session.add(challenge) + session.commit() + + result = user_challenges_repo.get_by_user_challenge_name("test_challenge") + assert result == challenge, "Challenge should be retrieved by name" + +def test_get_by_user_challenge_name_Invalid_name_failed(test_db): + """Test retrieving a challenge by name""" + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + challenge = UserChallenges( + username="test_user", + C_idx=1, + userChallengeName="test_challenge", + port=30000 + ) + session.add(challenge) + session.commit() + + result = user_challenges_repo.get_by_user_challenge_name("invalid_challenge") + assert result is None, "Invalid challenge name should return None" + + +# ============================ +# get_by_user_challenge_name +# ============================ + +def test_update_status_success(test_db): + """Test updating challenge status""" + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + challenge = UserChallenges( + username="test_user", + C_idx=1, + userChallengeName="test_challenge", + port=30000, + status="Running" + ) + session.add(challenge) + session.commit() + + success = user_challenges_repo.update_status(challenge, "Deleted") + assert success + assert challenge.status == "Deleted" + +# ============================ +# is_running +# ============================ + +def test_is_running_success(test_db): + """Test checking if challenge is running""" + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + challenge = UserChallenges( + username="test_user", + C_idx=1, + userChallengeName="test_challenge", + port=30000, + status="Running" + ) + + assert user_challenges_repo.is_running(challenge) + challenge.status = "Stopped" + assert not user_challenges_repo.is_running(challenge) + +# ============================ +# get_status +# ============================ + +def test_get_status_success(test_db): + """Test retrieving challenge status""" + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + challenge = UserChallenges( + username="test_user", + C_idx=1, + userChallengeName="test_challenge", + port=30000, + status="Running" + ) + session.add(challenge) + session.commit() + + result = user_challenges_repo.get_status(1, "test_user") + assert result == {"status": "Running", "port": 30000} + + challenge.status = "Deleted" + session.commit() + result = user_challenges_repo.get_status(1, "test_user") + assert result == {"status": "Deleted"} + +def test_get_status_invalid_challenge_failed(test_db): + """Test retrieving challenge status""" + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + challenge = UserChallenges( + username="test_user", + C_idx=1, + userChallengeName="test_challenge", + port=30000, + status="Running" + ) + session.add(challenge) + session.commit() + + # 잘못된 challenge id + result = user_challenges_repo.get_status(2, "test_user") + assert result is None + + # 잘못된 username + result2 = user_challenges_repo.get_status(1, "wrong_user") + assert result2 is None \ No newline at end of file