From 36e21e555a1c788f4679739e64735578dc344f39 Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 10:49:18 +0900 Subject: [PATCH 01/30] feature: ADD test scenario --- .gitignore | 2 ++ src/__init__.py | 0 tests/apps/test_repository.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 .gitignore create mode 100644 src/__init__.py create mode 100644 tests/apps/test_repository.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e5ac79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv +__pycache__ \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py new file mode 100644 index 0000000..3a2cd08 --- /dev/null +++ b/tests/apps/test_repository.py @@ -0,0 +1,21 @@ +import pytest + + +@pytest.mark.asyncio +async def test_user_repository_can_insert_data_with_valid(): + # given : 유요한 데이터(사용자 정보 + 이미지 정보) + + # when : DB에 데이터 저장 + + # then : 사용자 정보 + 이미지 경로 반환 + pass + + +@pytest.mark.asyncio +async def test_user_repository_cannot_insert_data_with_infalid(): + # give : 유요한 데이터(사용자 정보 + 이미지 정보) + + # when : DB에 데이터 저장 시 DB연결 오류 + + # then : 에러메시지 반환 + pass From 9c86ae7c56ada12d9e91b50235de7a60b65850ad Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 10:51:45 +0900 Subject: [PATCH 02/30] feature: ADD test scenario --- .gitignore | 3 ++- requirements.txt | 7 +++++++ tests/apps/test_repository.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 0e5ac79..652d35d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .venv -__pycache__ \ No newline at end of file +__pycache__ +.pytest_cache \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7119bd3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +exceptiongroup==1.2.0 +iniconfig==2.0.0 +packaging==23.2 +pluggy==1.3.0 +pytest==7.4.4 +pytest-asyncio==0.23.3 +tomli==2.0.1 diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index 3a2cd08..031de4b 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -15,7 +15,7 @@ async def test_user_repository_can_insert_data_with_valid(): async def test_user_repository_cannot_insert_data_with_infalid(): # give : 유요한 데이터(사용자 정보 + 이미지 정보) - # when : DB에 데이터 저장 시 DB연결 오류 + # when : DB에 데이터 저장 시 DB연결 오류 # then : 에러메시지 반환 pass From 1bffd414ecb0a9657f056e632339fb442a2d6ed0 Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 15:45:54 +0900 Subject: [PATCH 03/30] test: Add test to insert data with mongodb --- .gitignore | 4 ++- tests/apps/test_repository.py | 50 ++++++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 652d35d..7a9f0db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .venv __pycache__ -.pytest_cache \ No newline at end of file +.pytest_cache +conf +img \ No newline at end of file diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index 031de4b..df5dc90 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -1,21 +1,57 @@ +import os +import json import pytest +import pymongo +from datetime import datetime +from bson.binary import Binary + + +apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) +conf_path = os.path.abspath(os.path.join(apps_path, "conf")) +img_path = os.path.abspath(os.path.join(apps_path, "img")) + +conf_file = os.path.abspath(os.path.join(conf_path, "conf.json")) +with open(conf_file, "rt") as f: + conf = json.load(f) +DB_NAME = conf["mongo"]["DB_NAME"] +COLLECTION_NAME = conf["mongo"]["COLLECTION_NAME"] + +# MOCK data +USERNAME = "kim", +IMAGE_PATH = os.path.abspath(os.path.join(img_path, "test.jpg")) +ID = str(datetime.utcnow()) + + +@pytest.fixture +def mockup(): + with open(IMAGE_PATH, "rb") as f: + image_banary = Binary(f.read()) + yield { + "_id": ID, + "username": USERNAME, + "image": image_banary + } @pytest.mark.asyncio -async def test_user_repository_can_insert_data_with_valid(): - # given : 유요한 데이터(사용자 정보 + 이미지 정보) +async def test_user_repository_can_insert_data_with_valid(mockup): + # given : 유요한 데이터(사용자 정보 + 이미지 정보), 유효한 URL + CONNECTION_URL = conf["mongo"]["CONNECTION_URL"] # when : DB에 데이터 저장 + client = pymongo.MongoClient(CONNECTION_URL) + collection = client[DB_NAME][COLLECTION_NAME] + result = collection.insert_one(mockup) - # then : 사용자 정보 + 이미지 경로 반환 - pass + # then : 이미지 ID 반환 + assert result.inserted_id == ID @pytest.mark.asyncio -async def test_user_repository_cannot_insert_data_with_infalid(): - # give : 유요한 데이터(사용자 정보 + 이미지 정보) +async def test_user_repository_cannot_insert_data_with_valid(): + # give : 유요한 데이터(사용자 정보 + 이미지 정보), 잘못된 URL # when : DB에 데이터 저장 시 DB연결 오류 + # then : 에러 - # then : 에러메시지 반환 pass From 86618bcd0f28827da6bebdde6c42004a89768614 Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 15:53:47 +0900 Subject: [PATCH 04/30] test: ADD test fail connection to insert image with mongodb --- tests/apps/test_repository.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index df5dc90..5d83370 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -4,6 +4,7 @@ import pymongo from datetime import datetime from bson.binary import Binary +from pymongo.errors import ConnectionFailure apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) @@ -35,7 +36,7 @@ def mockup(): @pytest.mark.asyncio async def test_user_repository_can_insert_data_with_valid(mockup): - # given : 유요한 데이터(사용자 정보 + 이미지 정보), 유효한 URL + # given : 유효한 데이터(사용자 정보 + 이미지 정보), 유효한 URL CONNECTION_URL = conf["mongo"]["CONNECTION_URL"] # when : DB에 데이터 저장 @@ -49,9 +50,15 @@ async def test_user_repository_can_insert_data_with_valid(mockup): @pytest.mark.asyncio async def test_user_repository_cannot_insert_data_with_valid(): - # give : 유요한 데이터(사용자 정보 + 이미지 정보), 잘못된 URL + # give : 잘못된 URL + WRONG_URL = "wrong_url:!!" # when : DB에 데이터 저장 시 DB연결 오류 # then : 에러 + with pytest.raises(Exception): + client = pymongo.MongoClient(WRONG_URL) + collection = client[DB_NAME][COLLECTION_NAME] + result = collection.insert_one(mockup) - pass + # then : 이미지 ID 반환 + assert result.inserted_id == ID From a9dfb7f30d104c8c4f2266cc90cea7cd12e4b4a6 Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 16:41:24 +0900 Subject: [PATCH 05/30] feature: ADD Mongodb session manager --- src/libs/__init__.py | 9 +++++++++ src/libs/db_manager.py | 16 ++++++++++++++++ src/libs/exception.py | 11 +++++++++++ tests/apps/test_repository.py | 22 ++++++---------------- 4 files changed, 42 insertions(+), 16 deletions(-) create mode 100644 src/libs/__init__.py create mode 100644 src/libs/db_manager.py create mode 100644 src/libs/exception.py diff --git a/src/libs/__init__.py b/src/libs/__init__.py new file mode 100644 index 0000000..8b02a11 --- /dev/null +++ b/src/libs/__init__.py @@ -0,0 +1,9 @@ +import os +import json + +apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) +conf_path = os.path.abspath(os.path.join(apps_path, "conf")) + +conf_file = os.path.abspath(os.path.join(conf_path, "conf.json")) +with open(conf_file, "rt") as f: + conf = json.load(f) diff --git a/src/libs/db_manager.py b/src/libs/db_manager.py new file mode 100644 index 0000000..de6ddbd --- /dev/null +++ b/src/libs/db_manager.py @@ -0,0 +1,16 @@ +import pymongo +from . import conf +from .exception import DBConnectionError + + +class MongoManager: + def __init__(self, url: str = conf["mongo"]["CONNECTION_URL"]) -> None: + self.db = conf["mongo"]["DB_NAME"] + self.url = url + + def get_session(self): + try: + self.client = pymongo.MongoClient(self.url) + return self.client[self.db] + except Exception as e: + DBConnectionError(500, "Failed to connect Mongodb.", e) diff --git a/src/libs/exception.py b/src/libs/exception.py new file mode 100644 index 0000000..fd91e93 --- /dev/null +++ b/src/libs/exception.py @@ -0,0 +1,11 @@ +class CustomHttpException(Exception): + def __init__(self, code: int, message: str) -> None: + self.code = code + self.message = message + self.error = None + + +class DBConnectionError(CustomHttpException): + def __init__(self, code: int, message: str, err: Exception) -> None: + super().__init__(code, message) + self.error = err diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index 5d83370..c7d9d2c 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -1,23 +1,15 @@ import os -import json import pytest -import pymongo from datetime import datetime from bson.binary import Binary -from pymongo.errors import ConnectionFailure +from src.libs.db_manager import MongoManager apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) -conf_path = os.path.abspath(os.path.join(apps_path, "conf")) img_path = os.path.abspath(os.path.join(apps_path, "img")) -conf_file = os.path.abspath(os.path.join(conf_path, "conf.json")) -with open(conf_file, "rt") as f: - conf = json.load(f) -DB_NAME = conf["mongo"]["DB_NAME"] -COLLECTION_NAME = conf["mongo"]["COLLECTION_NAME"] - # MOCK data +COLLECTION_NAME = "tests" USERNAME = "kim", IMAGE_PATH = os.path.abspath(os.path.join(img_path, "test.jpg")) ID = str(datetime.utcnow()) @@ -37,11 +29,10 @@ def mockup(): @pytest.mark.asyncio async def test_user_repository_can_insert_data_with_valid(mockup): # given : 유효한 데이터(사용자 정보 + 이미지 정보), 유효한 URL - CONNECTION_URL = conf["mongo"]["CONNECTION_URL"] + db = MongoManager().get_session() # when : DB에 데이터 저장 - client = pymongo.MongoClient(CONNECTION_URL) - collection = client[DB_NAME][COLLECTION_NAME] + collection = db[COLLECTION_NAME] result = collection.insert_one(mockup) # then : 이미지 ID 반환 @@ -56,9 +47,8 @@ async def test_user_repository_cannot_insert_data_with_valid(): # when : DB에 데이터 저장 시 DB연결 오류 # then : 에러 with pytest.raises(Exception): - client = pymongo.MongoClient(WRONG_URL) - collection = client[DB_NAME][COLLECTION_NAME] + db = MongoManager(WRONG_URL).get_session() + collection = db[COLLECTION_NAME] result = collection.insert_one(mockup) - # then : 이미지 ID 반환 assert result.inserted_id == ID From 3e5e56816eb47e89a70fdf4f3d22ca219afab668 Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 16:51:42 +0900 Subject: [PATCH 06/30] feature: ADD code coverage --- .coverage | Bin 0 -> 53248 bytes .gitignore | 3 ++- requirements.txt | 4 ++++ src/libs/db_manager.py | 2 +- tests/apps/test_repository.py | 4 ++-- 5 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .coverage diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..320d6be6b18f62968ca186b075660a141eebbe28 GIT binary patch literal 53248 zcmeI)U2oe|7zc1WcH*W@dj?Y#m1^p_fv#HHtO5ybleT40L}Ai4nBW2l&q+>OkHpSw zXKODI$~H6!i4TB;_!jINz$J(~?hstCU4Q`buI+;7@k^7e&D36P9e*ov9Q&N(bAIP7 zZc@K_;j9}ex#|ZkCz8)=2Q*#RPD`n2T9KaP^vo8EHq7h|z3P42!#0cB;@hu{i?_5= z;bTp_JAO_qls+AQZtR;f}(*Z0HfB*y_@c$O*UMN}RsVV*X`;k*?swi;k zDoEDjFTJr`y|^qdR?nPWmdQSOcvRA|upp~4@UO|X3goKWRMPbtZrzDoZ%sz)Dhu5S zRf8uw+DB6z2V9Kf^Oo&4C|0D_D2aC9wwz#7zNa?l;{fT|BDE1EJ17z5u6Z0n9!{gr z$v~~DKzVf)Cb>B5Hs(g>ZhSCml@A`&Z>hLUPEe=E*g$D$N3SR=tCUdP4;q~NTHtu~ zbrsG_XRCyDpYpj8C8Z7gRweRf)AizFdZ8P+z9-d&s&^vQ*s*}~jFv5g*Iyb+>k=2} zQW7&gSD#KV@(wZ6;A!Ml%ADtCti6bQYKk1^+3U1wM1Hx`YIEgKjO4_TKP=|3xRIY0 zcGq>YLtXaDJ5Ze&IoD_stK+#>I%=!(=VdSEd|V9ApO;(+Y5~uV70SmB>Ke65wGm!v z(&sJP=|q0~-lod8muOYG`P7J2o|(~aU5OhC^Vl^v3b!_j1C3^D_mHN;$A`BY?j?hb zgnLo5DgD*B%W!O&R1US~{ytO5Fr0upn)K6vl+#Dr+Bmb62U%fBXAE<@eFssd+CEv844Vc}6XBx*WK#X@;`W?0kNY6AAs*5+t;s$i9;r}gW_ zxEZCjmA+U5O(?6$9fr~DP)j*sC&&hphYgZhjNT}j^^x=l6=%UJl&jOjij#)3m(q=q zu4P)~$w|GN^avi8_?aIl1n=o}fd?jn-Dkh;MC)>U-g!4=d^TSwpO_q$an6Fh#OuP1 zcXDx-Z%xJeEgJOsX$)kU_w=&-y4qa0>NIIIX*+H}U6#h+WCoBNu;EZ(r{;H}xc&0m zxOwv2)5~v?K<|BKPod`G;MBd(`(fzPw93;~cs$!HXV;yON1_(;DC>cugEW`U%^DAD23%YA60i9xd4xa8b!T*|eB_AU?WCPC8E5Q}CiRw+WjlSF z8)zgC9PG81b2Q-6r=9+G!ghTe{5ClQ4{k{f))Iv*RQg$GbdE3Zb#YIl2NnoG00Izz z00bZa0SG_<0uX=z1fDzrL(l0JU;pRC_nP>HR#+ea0SG_<0uX=z1Rwwb2tWV=5ZIpr zrJQj{WIypZDs*GIoc;*l#l;sEkB(BQa^kKg?utM6X91CG2tWV=5P$##AOHafKmY;| zfB*zCfs%1Z&;AOK8#Shj=^p|3`~UpJ=bHFJ+!V(r{-#YV5P$##AOHafKmY;|fB*y_ z0D;Ft;B?;9y7|hw-%^#pryIybE!V5G1OKwBN0n+2RnM<1yyArXS_y-CrRmnfirT2F zHsAAuZWB;7DFthcm0q1|DiuY00Izz00bZa0SG_<0uX=z1fFOCgMKwI&fov*;=V=? zED(SI1Rwwb2tWV=5P$##AOHafJb?m+FN;n7lH0mE#rP|NlQ5^(s>U literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore index 7a9f0db..cbdf6b5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__ .pytest_cache conf -img \ No newline at end of file +img +htmlcov \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7119bd3..3cbd33a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,11 @@ +coverage==7.4.0 +dnspython==2.5.0 exceptiongroup==1.2.0 iniconfig==2.0.0 packaging==23.2 pluggy==1.3.0 +pymongo==4.6.1 pytest==7.4.4 pytest-asyncio==0.23.3 +pytest-cov==4.1.0 tomli==2.0.1 diff --git a/src/libs/db_manager.py b/src/libs/db_manager.py index de6ddbd..69ac9fb 100644 --- a/src/libs/db_manager.py +++ b/src/libs/db_manager.py @@ -5,8 +5,8 @@ class MongoManager: def __init__(self, url: str = conf["mongo"]["CONNECTION_URL"]) -> None: - self.db = conf["mongo"]["DB_NAME"] self.url = url + self.db = conf["mongo"]["DB_NAME"] def get_session(self): try: diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index c7d9d2c..97a3977 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -44,8 +44,8 @@ async def test_user_repository_cannot_insert_data_with_valid(): # give : 잘못된 URL WRONG_URL = "wrong_url:!!" - # when : DB에 데이터 저장 시 DB연결 오류 - # then : 에러 + # when : DB에 데이터 저장 + # then : DB 연결 오류 with pytest.raises(Exception): db = MongoManager(WRONG_URL).get_session() collection = db[COLLECTION_NAME] From c1bfea650cf52810bb3716fceae4a9dfc3dac41d Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 17:29:38 +0900 Subject: [PATCH 07/30] feature: ADD repository to insert images with mongodb --- src/apps/__init__.py | 0 src/apps/repository.py | 14 ++++++++++++++ src/libs/exception.py | 6 ++++++ tests/apps/test_repository.py | 11 +++++------ 4 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 src/apps/__init__.py create mode 100644 src/apps/repository.py diff --git a/src/apps/__init__.py b/src/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/repository.py b/src/apps/repository.py new file mode 100644 index 0000000..0da90ba --- /dev/null +++ b/src/apps/repository.py @@ -0,0 +1,14 @@ +from src.libs.exception import DBProcessError + + +class Repository(): + def __init__(self, session: any) -> None: + self.session = session + + def insert_image(self, collection_name: str, img_data: dict) -> str: + try: + collection = self.session[collection_name] + result_id = collection.insert_one(img_data) + return result_id + except Exception as e: + DBProcessError(500, "Failed to insert data", e) diff --git a/src/libs/exception.py b/src/libs/exception.py index fd91e93..2355236 100644 --- a/src/libs/exception.py +++ b/src/libs/exception.py @@ -9,3 +9,9 @@ class DBConnectionError(CustomHttpException): def __init__(self, code: int, message: str, err: Exception) -> None: super().__init__(code, message) self.error = err + + +class DBProcessError(CustomHttpException): + def __init__(self, code: int, message: str, err: Exception) -> None: + super().__init__(code, message) + self.error = err diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index 97a3977..67ee8e3 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -3,6 +3,7 @@ from datetime import datetime from bson.binary import Binary from src.libs.db_manager import MongoManager +from src.apps.repository import Repository apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) @@ -29,11 +30,10 @@ def mockup(): @pytest.mark.asyncio async def test_user_repository_can_insert_data_with_valid(mockup): # given : 유효한 데이터(사용자 정보 + 이미지 정보), 유효한 URL - db = MongoManager().get_session() + session = MongoManager().get_session() # when : DB에 데이터 저장 - collection = db[COLLECTION_NAME] - result = collection.insert_one(mockup) + result = Repository(session).insert_image(COLLECTION_NAME, mockup) # then : 이미지 ID 반환 assert result.inserted_id == ID @@ -47,8 +47,7 @@ async def test_user_repository_cannot_insert_data_with_valid(): # when : DB에 데이터 저장 # then : DB 연결 오류 with pytest.raises(Exception): - db = MongoManager(WRONG_URL).get_session() - collection = db[COLLECTION_NAME] - result = collection.insert_one(mockup) + session = MongoManager(WRONG_URL).get_session() + result = Repository(session).insert_image(COLLECTION_NAME, mockup) assert result.inserted_id == ID From 5eaa43efaab400520ba81030d73825aefbec43b7 Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 18:42:28 +0900 Subject: [PATCH 08/30] feature: ADD error code enum class --- .coverage | Bin 53248 -> 53248 bytes src/apps/repository.py | 7 +++++-- src/libs/db_manager.py | 5 +++-- src/libs/error_code.py | 14 ++++++++++++++ src/libs/exception.py | 17 +++++++---------- tests/apps/test_repository.py | 2 +- 6 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 src/libs/error_code.py diff --git a/.coverage b/.coverage index 320d6be6b18f62968ca186b075660a141eebbe28..5da89848076877dfb513d6344ea414c9835a09cc 100644 GIT binary patch delta 526 zcmaKmy-Pw-7{<@_d|jh^UJEOT28W`CN@|YKatWlnwn>>o5GdCgTFMLd4|M;48XF72 zgdkeVEpjOdLU6E#J_sVPmfmxtAcB_XJiO<}!>_PM3w!i{#loibMBogHFss_Ctk%?o z^;GH#U0EV-GU7@vW)BbKopf|{Who!cI8N3{rL)VK@!UpS{sItc!U--pbj-$KL)R{%Y)KiHiRfOc84Ni6Y3g$LzM@(gC^WU Z9pO_Lp(Z63l{$nDu7l~IIs!Tjj8E0vjkf>* delta 300 zcmYj~y-EW?6h>!u<|eLY?-vCNZL|>^3k7Wyl881b+Wg&TjsjLiWWhE@x zF6G*3D#32S!ZZ;hia)HNGlgJt_~2a5ZTD2Wr>=B^iTeT>I@sn9?kBU=b1pSVi3dx*AGbW4bHj@fmqsyP1V#j7Kn09qc@?WJQ z$vhu!*-B8-O&!lWxolKgDNeHc0Mju(tEvf>U4ij)YH Lc2EwUgS7Ysxvx~s diff --git a/src/apps/repository.py b/src/apps/repository.py index 0da90ba..f1a72cc 100644 --- a/src/apps/repository.py +++ b/src/apps/repository.py @@ -1,4 +1,5 @@ -from src.libs.exception import DBProcessError +from src.libs.exception import DBError +from src.libs.error_code import DBErrorCode class Repository(): @@ -9,6 +10,8 @@ def insert_image(self, collection_name: str, img_data: dict) -> str: try: collection = self.session[collection_name] result_id = collection.insert_one(img_data) + if not result_id: + raise Exception return result_id except Exception as e: - DBProcessError(500, "Failed to insert data", e) + DBError(**DBErrorCode.DBProcessError, err=e) diff --git a/src/libs/db_manager.py b/src/libs/db_manager.py index 69ac9fb..523638b 100644 --- a/src/libs/db_manager.py +++ b/src/libs/db_manager.py @@ -1,6 +1,7 @@ import pymongo from . import conf -from .exception import DBConnectionError +from .exception import DBError +from .error_code import DBErrorCode class MongoManager: @@ -13,4 +14,4 @@ def get_session(self): self.client = pymongo.MongoClient(self.url) return self.client[self.db] except Exception as e: - DBConnectionError(500, "Failed to connect Mongodb.", e) + DBError(**DBErrorCode.DBConnectionError, err=e) diff --git a/src/libs/error_code.py b/src/libs/error_code.py new file mode 100644 index 0000000..737c434 --- /dev/null +++ b/src/libs/error_code.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class DBErrorCode(Enum): + DBConnectionError = { + "code": 500, + "message": "Failed to connect Mongodb. Contact service administrator.", + "log": "DB Connect Error. Check DB module." + } + DBProcessError = { + "code": 500, + "message": "Failed to insert data. Contact service administrator.", + "log": "DB Process Error. Check DB module." + } diff --git a/src/libs/exception.py b/src/libs/exception.py index 2355236..d66a692 100644 --- a/src/libs/exception.py +++ b/src/libs/exception.py @@ -1,17 +1,14 @@ class CustomHttpException(Exception): - def __init__(self, code: int, message: str) -> None: + def __init__(self, code: int, message: str, log: str) -> None: self.code = code self.message = message + self.log = log self.error = None -class DBConnectionError(CustomHttpException): - def __init__(self, code: int, message: str, err: Exception) -> None: - super().__init__(code, message) - self.error = err - - -class DBProcessError(CustomHttpException): - def __init__(self, code: int, message: str, err: Exception) -> None: - super().__init__(code, message) +class DBError(CustomHttpException): + def __init__( + self, code: int, message: str, log: str, err: Exception + ) -> None: + super().__init__(code, message, log) self.error = err diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index 67ee8e3..c66c608 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -44,9 +44,9 @@ async def test_user_repository_cannot_insert_data_with_valid(): # give : 잘못된 URL WRONG_URL = "wrong_url:!!" - # when : DB에 데이터 저장 # then : DB 연결 오류 with pytest.raises(Exception): + # when : DB에 데이터 저장 session = MongoManager(WRONG_URL).get_session() result = Repository(session).insert_image(COLLECTION_NAME, mockup) From e35cdd8b5325c5f9a85d9e9efbf2e23d88b62cff Mon Sep 17 00:00:00 2001 From: robert-min Date: Mon, 22 Jan 2024 19:57:41 +0900 Subject: [PATCH 09/30] feature: ADD service layater to insert image --- src/apps/repository.py | 2 ++ src/apps/service.py | 31 +++++++++++++++++++++++++++++++ src/libs/util.py | 28 ++++++++++++++++++++++++++++ tests/apps/test_service.py | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 src/apps/service.py create mode 100644 src/libs/util.py create mode 100644 tests/apps/test_service.py diff --git a/src/apps/repository.py b/src/apps/repository.py index f1a72cc..462314b 100644 --- a/src/apps/repository.py +++ b/src/apps/repository.py @@ -10,6 +10,8 @@ def insert_image(self, collection_name: str, img_data: dict) -> str: try: collection = self.session[collection_name] result_id = collection.insert_one(img_data) + + # TODO : 데이터가 들어가지 않는 경우가 존재!! connection pool setting 확인 필요 if not result_id: raise Exception return result_id diff --git a/src/apps/service.py b/src/apps/service.py new file mode 100644 index 0000000..07c36aa --- /dev/null +++ b/src/apps/service.py @@ -0,0 +1,31 @@ +from bson.binary import Binary +from fastapi import UploadFile +from .repository import Repository +from src.libs.util import save_image_local, make_unique_name + + +class Service: + def __init__(self, session) -> None: + self.repo = Repository(session) + + def insert_image(self, username: str, image_file: UploadFile) -> str: + file_name = make_unique_name(username, extension=".png") + + # img 파일의 재사용성을 고려해 로컬에 이미지 저장 + user_file_path = save_image_local(image_file, file_name) + + with open(user_file_path, "rb") as f: + image_banary = Binary(f.read()) + + data = { + "_id": file_name, + "username": username, + "image": image_banary + } + self.repo.insert_image("tests", data) + + # TODO : 응답 수정 + return { + "username": username + } + diff --git a/src/libs/util.py b/src/libs/util.py new file mode 100644 index 0000000..a5459bc --- /dev/null +++ b/src/libs/util.py @@ -0,0 +1,28 @@ +import os +from datetime import datetime +from fastapi import UploadFile + + +def create_folder_if_not_exists(folder_path: str): + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + +def make_unique_name(username: str, extension=".png") -> str: + current_time = datetime.now() + timestamp = current_time.strftime("%Y%m%d_%H%M%S") + + return f"{timestamp}_{username}{extension}" + + +def save_image_local(image_file: UploadFile, file_name: str) -> str: + libs_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) + imgs_path = os.path.abspath(os.path.join(libs_path, "imgs")) + create_folder_if_not_exists(imgs_path) + + user_file_path = os.path.abspath(os.path.join(imgs_path, file_name)) + + with open(user_file_path, "wb") as f: + f.write(image_file.file.read()) + + return user_file_path diff --git a/tests/apps/test_service.py b/tests/apps/test_service.py new file mode 100644 index 0000000..d607ebc --- /dev/null +++ b/tests/apps/test_service.py @@ -0,0 +1,35 @@ +import os +import pytest +from fastapi import FastAPI, UploadFile, File +from fastapi.testclient import TestClient +from src.apps.service import Service +from src.libs.db_manager import MongoManager + + +apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) +img_path = os.path.abspath(os.path.join(apps_path, "img")) + +# MOCK data +USERNAME = "kim" +IMAGE_PATH = os.path.abspath(os.path.join(img_path, "test.jpg")) + +app = FastAPI() + + +@app.post("/test/uploadfile/") +async def create_upload_file(file: UploadFile = File(...)): + session = MongoManager().get_session() + result = Service(session).insert_image(USERNAME, file) + return result + + +@pytest.mark.asyncio +async def test_user_service_can_insert_image_with_valid(): + client = TestClient(app) + + with open(IMAGE_PATH, "rb") as f: + files = {"file": ("image.jpg", f, "image/jpeg")} + response = client.post("/test/uploadfile/", files=files) + + assert response.status_code == 200 + assert response.json()["username"] == USERNAME From 12dfad58c1b0f6a9162235cf2ba4aebb85cdebe6 Mon Sep 17 00:00:00 2001 From: robert-min Date: Tue, 23 Jan 2024 14:31:05 +0900 Subject: [PATCH 10/30] feature: ADD libs/util & apps/repository testcode: --- .coverage | Bin 53248 -> 53248 bytes requirements.txt | 16 +++++++++++ src/apps/repository.py | 4 +-- src/apps/service.py | 10 +++++-- src/libs/db_manager.py | 2 +- src/libs/error_code.py | 8 ++++++ src/libs/exception.py | 8 ++++++ src/libs/util.py | 25 ++++++++++++++--- tests/apps/test_repository.py | 17 +++++++++++- tests/apps/test_service.py | 3 ++ tests/libs/test_error_code.py | 11 ++++++++ tests/libs/test_util.py | 51 ++++++++++++++++++++++++++++++++++ 12 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 tests/libs/test_error_code.py create mode 100644 tests/libs/test_util.py diff --git a/.coverage b/.coverage index 5da89848076877dfb513d6344ea414c9835a09cc..b221d12b4bd34ad6671259ccc4d7e8524d522791 100644 GIT binary patch delta 707 zcmZ{iO=uHQ5XX0Yn`Gbao1HXLwIcE3Qom}99*T;oSQSNF!Lz2=Hfo^Iu-OU{iH+EU zcxYikJeb&n5KmGFRuQ2GKfn(v#Y-WaFh*> zvN3I66hu+Mr+5WBt(rA%xmFzJpbTeWiex|g8M;J3f%i;w=M5WcV}a6D(iL) zxaq!anG1)7w`j-LH2NvW>36z%`g>C}-EA@@M~kgq<~WD?O2QyJwcgd5PiHdOST^OH zPxr``gPf*F({`iL^yZGmy>2_=#qGGa){azMdqgGnl7i202pUwpGeE2vA|fAEb%S^?#>-E81< zZ~oNwKm5r$e{y7f6wp<$hM(~RPU9p#!G}18xA6vE!^`Lr{|US#KvVf@nA~->*WLgj zK~-2(b<-rV%1m^eOuj_Jpkjp)eUOf|7+h+eKU2Gz(sj}iy;@>=NUG46y$L-iRWP5A z>H(<&rSHtrNJX`uuNH=nI-9NsLyYP$8)v~~-okJbah4Ta#IN`XtN4ysr|<{}s2nX*w2L7Y`E&R@WKl!%tmGBwzzS}G)aE*8JJ72@eNq$0;4gCx! z%lip!F7cbd2vq)_f&UnPE1wa+3*UC$_k5*%zktfG^G<#rYXDIm2UQ+7K~adEe;)(? zPyUbmFZgfqU*JE^zYnN*1-}+MI}0PH0DDnAI~$0>X2;LL3S str: result_id = collection.insert_one(img_data) # TODO : 데이터가 들어가지 않는 경우가 존재!! connection pool setting 확인 필요 - if not result_id: - raise Exception return result_id except Exception as e: - DBError(**DBErrorCode.DBProcessError, err=e) + raise DBError(**DBErrorCode.DBProcessError.value, err=e) diff --git a/src/apps/service.py b/src/apps/service.py index 07c36aa..051e108 100644 --- a/src/apps/service.py +++ b/src/apps/service.py @@ -1,7 +1,11 @@ from bson.binary import Binary from fastapi import UploadFile from .repository import Repository -from src.libs.util import save_image_local, make_unique_name +from src.libs.util import ( + save_image_local, + make_unique_name, + delete_file + ) class Service: @@ -24,8 +28,10 @@ def insert_image(self, username: str, image_file: UploadFile) -> str: } self.repo.insert_image("tests", data) + # 저장 완료 후 로컬 이미지 삭제 + delete_file(user_file_path) + # TODO : 응답 수정 return { "username": username } - diff --git a/src/libs/db_manager.py b/src/libs/db_manager.py index 523638b..64f11c9 100644 --- a/src/libs/db_manager.py +++ b/src/libs/db_manager.py @@ -14,4 +14,4 @@ def get_session(self): self.client = pymongo.MongoClient(self.url) return self.client[self.db] except Exception as e: - DBError(**DBErrorCode.DBConnectionError, err=e) + DBError(**DBErrorCode.DBConnectionError.value, err=e) diff --git a/src/libs/error_code.py b/src/libs/error_code.py index 737c434..bb23359 100644 --- a/src/libs/error_code.py +++ b/src/libs/error_code.py @@ -12,3 +12,11 @@ class DBErrorCode(Enum): "message": "Failed to insert data. Contact service administrator.", "log": "DB Process Error. Check DB module." } + + +class SystemErrorCode(Enum): + OSModuleError = { + "code": 500, + "message": "System errpr. Contact service administrator.", + "log": "OSModule Error. Check os library" + } diff --git a/src/libs/exception.py b/src/libs/exception.py index d66a692..8d7203c 100644 --- a/src/libs/exception.py +++ b/src/libs/exception.py @@ -7,6 +7,14 @@ def __init__(self, code: int, message: str, log: str) -> None: class DBError(CustomHttpException): + def __init__( + self, code: int, message: str, log: str, err: Exception = None + ) -> None: + super().__init__(code, message, log) + self.error = err + + +class SystemError(CustomHttpException): def __init__( self, code: int, message: str, log: str, err: Exception ) -> None: diff --git a/src/libs/util.py b/src/libs/util.py index a5459bc..a0e5db0 100644 --- a/src/libs/util.py +++ b/src/libs/util.py @@ -1,11 +1,18 @@ import os from datetime import datetime from fastapi import UploadFile +from .exception import SystemError +from .error_code import SystemErrorCode -def create_folder_if_not_exists(folder_path: str): - if not os.path.exists(folder_path): - os.makedirs(folder_path) +def create_folder_if_not_exists(folder_path: str) -> str: + try: + if not os.path.exists(folder_path): + os.makedirs(folder_path) + return "Success create folder." + return "Already exist folder." + except OSError as e: + raise SystemError(**SystemErrorCode.OSModuleError, err=e) def make_unique_name(username: str, extension=".png") -> str: @@ -17,7 +24,7 @@ def make_unique_name(username: str, extension=".png") -> str: def save_image_local(image_file: UploadFile, file_name: str) -> str: libs_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) - imgs_path = os.path.abspath(os.path.join(libs_path, "imgs")) + imgs_path = os.path.abspath(os.path.join(libs_path, "img")) create_folder_if_not_exists(imgs_path) user_file_path = os.path.abspath(os.path.join(imgs_path, file_name)) @@ -26,3 +33,13 @@ def save_image_local(image_file: UploadFile, file_name: str) -> str: f.write(image_file.file.read()) return user_file_path + + +def delete_file(file_path): + try: + if os.path.exists(file_path): + os.remove(file_path) + return "Success delete file." + return "There is no file." + except OSError as e: + raise SystemError(**SystemErrorCode.OSModuleError, err=e) diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index c66c608..3b101cd 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -4,6 +4,7 @@ from bson.binary import Binary from src.libs.db_manager import MongoManager from src.apps.repository import Repository +from src.libs.exception import DBError apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) @@ -27,6 +28,7 @@ def mockup(): } +@pytest.mark.order(1) @pytest.mark.asyncio async def test_user_repository_can_insert_data_with_valid(mockup): # given : 유효한 데이터(사용자 정보 + 이미지 정보), 유효한 URL @@ -39,13 +41,26 @@ async def test_user_repository_can_insert_data_with_valid(mockup): assert result.inserted_id == ID +@pytest.mark.order(2) +@pytest.mark.asyncio +async def test_user_repository_cannot_insert_data_with_invalid(mockup): + # given : 동일한 ID를 가진 데이터, 유효한 URL + session = MongoManager().get_session() + + # then : DBError + with pytest.raises(DBError): + # when : DB에 데이터 저장 + result = Repository(session).insert_image(COLLECTION_NAME, mockup) + assert result.inserted_id == ID + + @pytest.mark.asyncio async def test_user_repository_cannot_insert_data_with_valid(): # give : 잘못된 URL WRONG_URL = "wrong_url:!!" # then : DB 연결 오류 - with pytest.raises(Exception): + with pytest.raises(DBError): # when : DB에 데이터 저장 session = MongoManager(WRONG_URL).get_session() result = Repository(session).insert_image(COLLECTION_NAME, mockup) diff --git a/tests/apps/test_service.py b/tests/apps/test_service.py index d607ebc..11f334e 100644 --- a/tests/apps/test_service.py +++ b/tests/apps/test_service.py @@ -27,9 +27,12 @@ async def create_upload_file(file: UploadFile = File(...)): async def test_user_service_can_insert_image_with_valid(): client = TestClient(app) + # given : 유효한 데이터(이미지) with open(IMAGE_PATH, "rb") as f: files = {"file": ("image.jpg", f, "image/jpeg")} + # when : DB에 저장 response = client.post("/test/uploadfile/", files=files) + # then : 정상 처리 assert response.status_code == 200 assert response.json()["username"] == USERNAME diff --git a/tests/libs/test_error_code.py b/tests/libs/test_error_code.py new file mode 100644 index 0000000..09b3e0c --- /dev/null +++ b/tests/libs/test_error_code.py @@ -0,0 +1,11 @@ +import pytest +from src.libs.error_code import DBErrorCode + + +@pytest.mark.asyncio +async def test_DBErrorCode_enum_class(): + assert DBErrorCode.DBProcessError.value == { + "code": 500, + "message": "Failed to insert data. Contact service administrator.", + "log": "DB Process Error. Check DB module." + } diff --git a/tests/libs/test_util.py b/tests/libs/test_util.py new file mode 100644 index 0000000..1774e24 --- /dev/null +++ b/tests/libs/test_util.py @@ -0,0 +1,51 @@ +import os +import pytest +from src.libs.util import create_folder_if_not_exists, delete_file + +# Mock +libs_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) +test_path = os.path.abspath(os.path.join(libs_path, "test")) +file_path = os.path.abspath(os.path.join(test_path, "test.txt")) + + +@pytest.mark.order(1) +@pytest.mark.asyncio +async def test_create_folder_and_file(): + # given : 유효한 경로(현재 경로에 새로운 폴더 생성) + # when : 폴더 생성 + result = create_folder_if_not_exists(test_path) + + # then : Success create folder. 반환 + assert result == "Success create folder." + + # given : 이미 생성된 경로 + # when : 폴더 생성 + result = create_folder_if_not_exists(test_path) + + # then : Already exist folder. 반환 + assert result == "Already exist folder." + + # 파일 생성 + with open(file_path, "a") as f: + f.write("hello\n") + + +@pytest.mark.order(2) +@pytest.mark.asyncio +async def test_delete_file(): + # given : 유효한 경로(test 폴더 안에 유효한 파일 존재) + # when : 파일 삭제 + result = delete_file(file_path) + + # then : Success delete file. 반환 + assert result == "Success delete file." + + # given : 이미 삭제된 파일 + # when : 파일 삭제 + result = delete_file(file_path) + + # then : There is no file. 반환 + assert result == "There is no file." + + if os.path.exists(test_path): + os.removedirs(test_path) From 3f901035de0bb1c5c6d7d1e97571480e7958af7f Mon Sep 17 00:00:00 2001 From: robert-min Date: Tue, 23 Jan 2024 15:54:01 +0900 Subject: [PATCH 11/30] test: ADD controller test scenario --- src/libs/util.py | 4 ++-- tests/apps/test_controller.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 tests/apps/test_controller.py diff --git a/src/libs/util.py b/src/libs/util.py index a0e5db0..5dd9dcb 100644 --- a/src/libs/util.py +++ b/src/libs/util.py @@ -12,7 +12,7 @@ def create_folder_if_not_exists(folder_path: str) -> str: return "Success create folder." return "Already exist folder." except OSError as e: - raise SystemError(**SystemErrorCode.OSModuleError, err=e) + raise SystemError(**SystemErrorCode.OSModuleError.value, err=e) def make_unique_name(username: str, extension=".png") -> str: @@ -42,4 +42,4 @@ def delete_file(file_path): return "Success delete file." return "There is no file." except OSError as e: - raise SystemError(**SystemErrorCode.OSModuleError, err=e) + raise SystemError(**SystemErrorCode.OSModuleError.value, err=e) diff --git a/tests/apps/test_controller.py b/tests/apps/test_controller.py new file mode 100644 index 0000000..7fa641e --- /dev/null +++ b/tests/apps/test_controller.py @@ -0,0 +1,33 @@ +import pytest + +# TODO : 작품이 아닌 일반 사진을 넣었을 때 어떻게 처리할 것인지 고민!! + + +@pytest.mark.asyncio +async def test_user_can_make_generate_content(): + # given : 유효한 payload (header : username, file: UpladFile) + + # when : 콘텐츠 생성 API 요청 + + # given : 정상 응답 username + pass + + +@pytest.mark.asyncio +async def test_user_cannot_make_generate_content_with_non_header(): + # given : 유효하지 않은 payload (header 없이 요청) + + # when : 콘텐츠 생성 API 요청 + + # given : 에러메시지 + pass + + +@pytest.mark.asyncio +async def test_user_cannot_make_generate_content_with_non_file(): + # given : 유효하지 않은 payload (file 없이 요청) + + # when : 콘텐츠 생성 API 요청 + + # given : 에러메시지 + pass From 14f0ea718eaa1bf916cfba644a885d4a1f08b6ee Mon Sep 17 00:00:00 2001 From: robert-min Date: Tue, 23 Jan 2024 17:20:56 +0900 Subject: [PATCH 12/30] test: ADD controller test code --- src/libs/error_code.py | 15 ++++- src/libs/exception.py | 5 ++ tests/apps/test_controller.py | 111 +++++++++++++++++++++++++++++++--- 3 files changed, 120 insertions(+), 11 deletions(-) diff --git a/src/libs/error_code.py b/src/libs/error_code.py index bb23359..2e21bf5 100644 --- a/src/libs/error_code.py +++ b/src/libs/error_code.py @@ -17,6 +17,19 @@ class DBErrorCode(Enum): class SystemErrorCode(Enum): OSModuleError = { "code": 500, - "message": "System errpr. Contact service administrator.", + "message": "System error. Contact service administrator.", "log": "OSModule Error. Check os library" } + + +class UserRequestErrorCode(Enum): + NonHeaderError = { + "code": 401, + "message": "There is non header. Please log in again.", + "log": "User request fail with non header." + } + NonFileError = { + "code": 401, + "message": "There is non file. Please request again.", + "log": "User request fail with non file." + } diff --git a/src/libs/exception.py b/src/libs/exception.py index 8d7203c..2d6656e 100644 --- a/src/libs/exception.py +++ b/src/libs/exception.py @@ -20,3 +20,8 @@ def __init__( ) -> None: super().__init__(code, message, log) self.error = err + + +class UserError(CustomHttpException): + def __init__(self, code: int, message: str, log: str) -> None: + super().__init__(code, message, log) diff --git a/tests/apps/test_controller.py b/tests/apps/test_controller.py index 7fa641e..1cd7566 100644 --- a/tests/apps/test_controller.py +++ b/tests/apps/test_controller.py @@ -1,33 +1,124 @@ +import os import pytest +from fastapi import FastAPI, UploadFile, File, Header +from fastapi.testclient import TestClient +from src.libs.db_manager import MongoManager +from src.apps.service import Service +from src.libs.exception import UserError +from src.libs.error_code import UserRequestErrorCode +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from src.libs.exception import CustomHttpException + # TODO : 작품이 아닌 일반 사진을 넣었을 때 어떻게 처리할 것인지 고민!! +# Mock data +apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) +img_path = os.path.abspath(os.path.join(apps_path, "img")) + +USERNAME = "kim" +IMAGE_PATH = os.path.abspath(os.path.join(img_path, "test.jpg")) + + +app = FastAPI() + + +def error_handlers(app): + @app.exception_handler(CustomHttpException) + async def http_custom_exception_handler(request: Request, exc: CustomHttpException): + content = { + "meta": { + "code": exc.code, + "error": str(exc.error), + "message": exc.message + }, + "data": None + } + return JSONResponse( + status_code=exc.code, + content=content + ) + + +error_handlers(app) + + +@app.post("/user/content") +async def make_generated_content( + username: str = Header(default=None), + file: UploadFile = File(default=None) + ): + # TODO : controller 이전 시 의존성으로 처리 + session = MongoManager().get_session() + + if not username: + raise UserError(**UserRequestErrorCode.NonHeaderError.value) + if not file: + raise UserError(**UserRequestErrorCode.NonFileError.value) + + result = Service(session).insert_image(username, file) + + return { + "meta": { + "code": 200, + "message": "ok" + }, + "data": result + } + + +@pytest.fixture +def client(): + client = TestClient(app) + yield client + @pytest.mark.asyncio -async def test_user_can_make_generate_content(): +async def test_user_can_make_generated_content(client): # given : 유효한 payload (header : username, file: UpladFile) + headers = {"username": USERNAME} - # when : 콘텐츠 생성 API 요청 + with open(IMAGE_PATH, "rb") as f: + files = {"file": ("image.jpg", f, "image/jpeg")} + + # when : 콘텐츠 생성 API 요청 + response = client.post( + "/user/content", + headers=headers, + files=files) - # given : 정상 응답 username - pass + # then : 정상 응답 username + assert response.status_code == 200 + assert response.json()["meta"]["message"] == "ok" + assert response.json()["data"]["username"] == USERNAME @pytest.mark.asyncio -async def test_user_cannot_make_generate_content_with_non_header(): +async def test_user_cannot_make_generated_content_with_non_header(client): # given : 유효하지 않은 payload (header 없이 요청) - # when : 콘텐츠 생성 API 요청 + with open(IMAGE_PATH, "rb") as f: + files = {"file": ("image.jpg", f, "image/jpeg")} - # given : 에러메시지 - pass + # when : 콘텐츠 생성 API 요청 + response = client.post( + "/user/content", + files=files) + assert response.status_code == 401 + assert response.json()["meta"]["message"] == "There is non header. Please log in again." @pytest.mark.asyncio -async def test_user_cannot_make_generate_content_with_non_file(): +async def test_user_cannot_make_generated_content_with_non_file(client): # given : 유효하지 않은 payload (file 없이 요청) + headers = {"username": USERNAME} # when : 콘텐츠 생성 API 요청 + response = client.post( + "/user/content", + headers=headers) # given : 에러메시지 - pass + assert response.status_code == 401 + assert response.json()["meta"]["message"] == "There is non file. Please request again." From 6f6f5ea6f97c039c53daa6f331952806682ad448 Mon Sep 17 00:00:00 2001 From: robert-min Date: Tue, 23 Jan 2024 17:43:12 +0900 Subject: [PATCH 13/30] feature: ADD apps/controller layer make_generated_content api --- app.py | 18 +++++++++++ src/apps/__init__.py | 24 +++++++++++++++ src/apps/controller.py | 29 ++++++++++++++++++ src/libs/error_handler.py | 23 ++++++++++++++ tests/apps/test_controller.py | 57 ++--------------------------------- 5 files changed, 96 insertions(+), 55 deletions(-) create mode 100644 app.py create mode 100644 src/apps/controller.py create mode 100644 src/libs/error_handler.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..4b7d63b --- /dev/null +++ b/app.py @@ -0,0 +1,18 @@ +import os +import sys +import uvicorn +root_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) +src_path = os.path.abspath(os.path.join(root_path, "src")) +apps_path = os.path.abspath(os.path.join(src_path, "apps")) +# libs_path = os.path.abspath(os.path.join(src_path, "libs")) + +if src_path not in sys.path: + sys.path.append(src_path) +if apps_path not in sys.path: + sys.path.append(apps_path) + + +if __name__ == "__main__": + from src.apps import create_app + app = create_app() + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/apps/__init__.py b/src/apps/__init__.py index e69de29..9484b06 100644 --- a/src/apps/__init__.py +++ b/src/apps/__init__.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from .controller import user +from src.libs.error_handler import error_handlers + + +def create_app(): + app = FastAPI() + + # Router + app.include_router(user) + + # Handler + error_handlers(app) + + # CORS + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + return app diff --git a/src/apps/controller.py b/src/apps/controller.py new file mode 100644 index 0000000..ad6e3a8 --- /dev/null +++ b/src/apps/controller.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter, Depends, UploadFile, File, Header +from src.libs.db_manager import MongoManager +from src.libs.exception import UserError +from src.libs.error_code import UserRequestErrorCode +from src.apps.service import Service + +user = APIRouter(prefix="/user") + + +@user.post('/content') +async def make_generated_content( + username: str = Header(default=None), + file: UploadFile = File(default=None) +): + # TODO : mongodb 의존성 추가 + session = MongoManager().get_session() + if not username: + raise UserError(**UserRequestErrorCode.NonHeaderError.value) + if not file: + raise UserError(**UserRequestErrorCode.NonFileError.value) + + result = Service(session).insert_image(username, file) + return { + "meta": { + "code": 200, + "message": "ok" + }, + "data": result + } diff --git a/src/libs/error_handler.py b/src/libs/error_handler.py new file mode 100644 index 0000000..547f9de --- /dev/null +++ b/src/libs/error_handler.py @@ -0,0 +1,23 @@ +from fastapi import Request +from src.libs.exception import CustomHttpException +from fastapi.responses import JSONResponse + + +def error_handlers(app) -> JSONResponse: + @app.exception_handler(CustomHttpException) + async def http_custom_exception_handler( + request: Request, + exc: CustomHttpException + ): + content = { + "meta": { + "code": exc.code, + "error": str(exc.error), + "message": exc.message + }, + "data": None + } + return JSONResponse( + status_code=exc.code, + content=content + ) diff --git a/tests/apps/test_controller.py b/tests/apps/test_controller.py index 1cd7566..3775ba1 100644 --- a/tests/apps/test_controller.py +++ b/tests/apps/test_controller.py @@ -1,14 +1,7 @@ import os import pytest -from fastapi import FastAPI, UploadFile, File, Header from fastapi.testclient import TestClient -from src.libs.db_manager import MongoManager -from src.apps.service import Service -from src.libs.exception import UserError -from src.libs.error_code import UserRequestErrorCode -from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse -from src.libs.exception import CustomHttpException +from src.apps import create_app # TODO : 작품이 아닌 일반 사진을 넣었을 때 어떻게 처리할 것인지 고민!! @@ -21,55 +14,9 @@ IMAGE_PATH = os.path.abspath(os.path.join(img_path, "test.jpg")) -app = FastAPI() - - -def error_handlers(app): - @app.exception_handler(CustomHttpException) - async def http_custom_exception_handler(request: Request, exc: CustomHttpException): - content = { - "meta": { - "code": exc.code, - "error": str(exc.error), - "message": exc.message - }, - "data": None - } - return JSONResponse( - status_code=exc.code, - content=content - ) - - -error_handlers(app) - - -@app.post("/user/content") -async def make_generated_content( - username: str = Header(default=None), - file: UploadFile = File(default=None) - ): - # TODO : controller 이전 시 의존성으로 처리 - session = MongoManager().get_session() - - if not username: - raise UserError(**UserRequestErrorCode.NonHeaderError.value) - if not file: - raise UserError(**UserRequestErrorCode.NonFileError.value) - - result = Service(session).insert_image(username, file) - - return { - "meta": { - "code": 200, - "message": "ok" - }, - "data": result - } - - @pytest.fixture def client(): + app = create_app() client = TestClient(app) yield client From 78374de5974785053d7fc52e6210d25e6b640f7e Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 09:38:55 +0900 Subject: [PATCH 14/30] fix: ADD async to I/O process --- .coverage | Bin 53248 -> 53248 bytes src/apps/controller.py | 2 +- src/apps/service.py | 7 +++---- src/libs/util.py | 4 ++-- tests/apps/test_service.py | 2 +- tests/libs/test_util.py | 4 ++-- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.coverage b/.coverage index b221d12b4bd34ad6671259ccc4d7e8524d522791..6becc9bf6490f2a63e8f387d5bf3f182f6cb0401 100644 GIT binary patch delta 879 zcmZvbTSyd97{_OJW@mS9=Ra$%M$l#vN_1P%D6LjkGg@->u>`N^X4c}$b^%j12kRjS z0tZB))`cE?kU|ipqV%B)sbNJg5rkA=6y1$Xg6N#pXpvsN^Z&l{|Ne&`!*nHtu7ofw z7H7NEJ`VSB4t{Xt4(TAf^q=~u-md3q@3aB!utwA=wQs~_w5oQmOW>L#mC6WhvD`}5mP*Aq5sIE}X$;Yg1A2;=U1nOZ%JZ0MRb!;} zWHb^EhoaQCPo+MchIrFL(P$)EceJ6kY3Zs^v9<;aZGPGM67)+xCpGfaxY=4)VYjXX z6)U∓RQ&vB37N_Da>}7pOU3`S*lnR(4bLeopPj1Dr>RKBg~%C*!0*&nKPw8STB6 z(5f`3(-b|Tin&5FdnL!QMDDWU)uoaV5KVV-oFvD7(Zt_&B{@m;E|XXPhg>TYO`U~! z-7Cq}-9iVu*~u%`=b92pE)q?i4N}-^C+)?eDI^0JIxkY|jui3{v#vX9qk#a2MSQ{= zyucIs{cqwLuA&zg(TOnXaR>nlz8+^aL*(T(#!SJ5riu-^1GF>&OqzmJFW;10QleTyu z#@l&OmWfT_IL<&1hhO-PkC?+Op5rN|a2K~Rit89e0{!Trl*izf=sIs0$pc6pgp3Rh z)MRD5FvLQB;441hEned#4SI$dOyV9UaEpcwV~EC>NEhf2q6-EqV1>|PO+5Dh0ktCS Ax&QzG delta 611 zcmYL^O=uHA7>0K>+ya2Y*naD1Ot5J?-DS5=UT{cgq{}CdDT2 zr?|lvc^#ckabtvrw)+2)GW(}2WMgdbzcC*oWmQ==b`vzzSB#xKG=${=G!5?A-?RO-QSYbvO&j|wPh(1p+N4qn0nJb*he3)kT?oQE?|LcjfRnhUDi z>%_vgbK}_!!s3{G-eInbu*Qui#&S==aWJfQqPGjNwT`XWt{m!K$uoxB!#Z`sEJfCr z9dTwVGAorRGZY#1AH<_bQTk{8>xJ}0?^vC_N3W=G6o0y0$8Q$NS#HTUdlwSbl+bKE_MiSge6;KTG}v Do?Eg) diff --git a/src/apps/controller.py b/src/apps/controller.py index ad6e3a8..e27003b 100644 --- a/src/apps/controller.py +++ b/src/apps/controller.py @@ -19,7 +19,7 @@ async def make_generated_content( if not file: raise UserError(**UserRequestErrorCode.NonFileError.value) - result = Service(session).insert_image(username, file) + result = await Service(session).insert_image(username, file) return { "meta": { "code": 200, diff --git a/src/apps/service.py b/src/apps/service.py index 051e108..b6d83a8 100644 --- a/src/apps/service.py +++ b/src/apps/service.py @@ -12,11 +12,11 @@ class Service: def __init__(self, session) -> None: self.repo = Repository(session) - def insert_image(self, username: str, image_file: UploadFile) -> str: + async def insert_image(self, username: str, image_file: UploadFile) -> str: file_name = make_unique_name(username, extension=".png") # img 파일의 재사용성을 고려해 로컬에 이미지 저장 - user_file_path = save_image_local(image_file, file_name) + user_file_path = await save_image_local(image_file, file_name) with open(user_file_path, "rb") as f: image_banary = Binary(f.read()) @@ -29,9 +29,8 @@ def insert_image(self, username: str, image_file: UploadFile) -> str: self.repo.insert_image("tests", data) # 저장 완료 후 로컬 이미지 삭제 - delete_file(user_file_path) + await delete_file(user_file_path) - # TODO : 응답 수정 return { "username": username } diff --git a/src/libs/util.py b/src/libs/util.py index 5dd9dcb..e12f5fc 100644 --- a/src/libs/util.py +++ b/src/libs/util.py @@ -22,7 +22,7 @@ def make_unique_name(username: str, extension=".png") -> str: return f"{timestamp}_{username}{extension}" -def save_image_local(image_file: UploadFile, file_name: str) -> str: +async def save_image_local(image_file: UploadFile, file_name: str) -> str: libs_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) imgs_path = os.path.abspath(os.path.join(libs_path, "img")) create_folder_if_not_exists(imgs_path) @@ -35,7 +35,7 @@ def save_image_local(image_file: UploadFile, file_name: str) -> str: return user_file_path -def delete_file(file_path): +async def delete_file(file_path): try: if os.path.exists(file_path): os.remove(file_path) diff --git a/tests/apps/test_service.py b/tests/apps/test_service.py index 11f334e..6c9312c 100644 --- a/tests/apps/test_service.py +++ b/tests/apps/test_service.py @@ -19,7 +19,7 @@ @app.post("/test/uploadfile/") async def create_upload_file(file: UploadFile = File(...)): session = MongoManager().get_session() - result = Service(session).insert_image(USERNAME, file) + result = await Service(session).insert_image(USERNAME, file) return result diff --git a/tests/libs/test_util.py b/tests/libs/test_util.py index 1774e24..9ed4d68 100644 --- a/tests/libs/test_util.py +++ b/tests/libs/test_util.py @@ -35,14 +35,14 @@ async def test_create_folder_and_file(): async def test_delete_file(): # given : 유효한 경로(test 폴더 안에 유효한 파일 존재) # when : 파일 삭제 - result = delete_file(file_path) + result = await delete_file(file_path) # then : Success delete file. 반환 assert result == "Success delete file." # given : 이미 삭제된 파일 # when : 파일 삭제 - result = delete_file(file_path) + result = await delete_file(file_path) # then : There is no file. 반환 assert result == "There is no file." From b84278ecc6bdff99d7bc20b2877e7eaa946132b7 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 09:49:47 +0900 Subject: [PATCH 15/30] fix: ADD mongodb client dependency. --- .coverage | Bin 53248 -> 53248 bytes src/apps/controller.py | 6 +++--- src/libs/db_manager.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.coverage b/.coverage index 6becc9bf6490f2a63e8f387d5bf3f182f6cb0401..b8b9e307ace5068d7ef676231e28af11fdc218e9 100644 GIT binary patch delta 18 acmZozz}&EadBdxI=DMHvHoxr`b^riXfeA_g delta 18 acmZozz}&EadBdxI=6yEzH^1!{b^riUTnNqp diff --git a/src/apps/controller.py b/src/apps/controller.py index e27003b..b010afa 100644 --- a/src/apps/controller.py +++ b/src/apps/controller.py @@ -1,3 +1,4 @@ +from pymongo import MongoClient from fastapi import APIRouter, Depends, UploadFile, File, Header from src.libs.db_manager import MongoManager from src.libs.exception import UserError @@ -10,10 +11,9 @@ @user.post('/content') async def make_generated_content( username: str = Header(default=None), - file: UploadFile = File(default=None) + file: UploadFile = File(default=None), + session: MongoClient = Depends(MongoManager().get_session) ): - # TODO : mongodb 의존성 추가 - session = MongoManager().get_session() if not username: raise UserError(**UserRequestErrorCode.NonHeaderError.value) if not file: diff --git a/src/libs/db_manager.py b/src/libs/db_manager.py index 64f11c9..5a26072 100644 --- a/src/libs/db_manager.py +++ b/src/libs/db_manager.py @@ -9,7 +9,7 @@ def __init__(self, url: str = conf["mongo"]["CONNECTION_URL"]) -> None: self.url = url self.db = conf["mongo"]["DB_NAME"] - def get_session(self): + def get_session(self) -> pymongo.MongoClient: try: self.client = pymongo.MongoClient(self.url) return self.client[self.db] From b1990d0ffe29cac0558af71a5563dafe3f70cb4f Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 10:02:59 +0900 Subject: [PATCH 16/30] fix: DELETE OSError and CHECK code coverage --- .coverage | Bin 53248 -> 53248 bytes src/libs/exception.py | 8 -------- src/libs/util.py | 25 ++++++++----------------- tests/apps/test_repository.py | 6 ++---- 4 files changed, 10 insertions(+), 29 deletions(-) diff --git a/.coverage b/.coverage index b8b9e307ace5068d7ef676231e28af11fdc218e9..eb63146dc08042a3c55c09c04748bd7e809fc152 100644 GIT binary patch delta 92 zcmZozz}&Eac>`O6k~ahYPyToOkNI!$U*JE+zl(nz{}TQg{C)g&{CWII{E__Ln*{}w w`6qk!8!~w_Pp;{g;^E?AVdNC!dRJVjD?fR0zY-@08&E)qt*&nK`F=wO0CUV6_y7O^ delta 94 zcmZozz}&Eac>`O6k|zWIPyToOkNI!$U*JE=zmtD0|6=}W{5||N{5kvy{Nenbn*{|F y`6qk#8!}B}n_Sl~#mmjb!pJGXWoI7~cbb3ll71yFPBx&32wPp9+~)KBh7JIyW*kNU diff --git a/src/libs/exception.py b/src/libs/exception.py index 2d6656e..04bda01 100644 --- a/src/libs/exception.py +++ b/src/libs/exception.py @@ -14,14 +14,6 @@ def __init__( self.error = err -class SystemError(CustomHttpException): - def __init__( - self, code: int, message: str, log: str, err: Exception - ) -> None: - super().__init__(code, message, log) - self.error = err - - class UserError(CustomHttpException): def __init__(self, code: int, message: str, log: str) -> None: super().__init__(code, message, log) diff --git a/src/libs/util.py b/src/libs/util.py index e12f5fc..8189f2d 100644 --- a/src/libs/util.py +++ b/src/libs/util.py @@ -1,18 +1,12 @@ import os from datetime import datetime from fastapi import UploadFile -from .exception import SystemError -from .error_code import SystemErrorCode - def create_folder_if_not_exists(folder_path: str) -> str: - try: - if not os.path.exists(folder_path): - os.makedirs(folder_path) - return "Success create folder." - return "Already exist folder." - except OSError as e: - raise SystemError(**SystemErrorCode.OSModuleError.value, err=e) + if not os.path.exists(folder_path): + os.makedirs(folder_path) + return "Success create folder." + return "Already exist folder." def make_unique_name(username: str, extension=".png") -> str: @@ -36,10 +30,7 @@ async def save_image_local(image_file: UploadFile, file_name: str) -> str: async def delete_file(file_path): - try: - if os.path.exists(file_path): - os.remove(file_path) - return "Success delete file." - return "There is no file." - except OSError as e: - raise SystemError(**SystemErrorCode.OSModuleError.value, err=e) + if os.path.exists(file_path): + os.remove(file_path) + return "Success delete file." + return "There is no file." diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index 3b101cd..ca57b9a 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -50,8 +50,7 @@ async def test_user_repository_cannot_insert_data_with_invalid(mockup): # then : DBError with pytest.raises(DBError): # when : DB에 데이터 저장 - result = Repository(session).insert_image(COLLECTION_NAME, mockup) - assert result.inserted_id == ID + Repository(session).insert_image(COLLECTION_NAME, mockup) @pytest.mark.asyncio @@ -63,6 +62,5 @@ async def test_user_repository_cannot_insert_data_with_valid(): with pytest.raises(DBError): # when : DB에 데이터 저장 session = MongoManager(WRONG_URL).get_session() - result = Repository(session).insert_image(COLLECTION_NAME, mockup) + Repository(session).insert_image(COLLECTION_NAME, mockup) - assert result.inserted_id == ID From 7c6da953306165d325b1db0658806d9fb88dc8e2 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 10:15:11 +0900 Subject: [PATCH 17/30] feature: ADD codecov workflow --- .github/workflows/coverage.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..53160a0 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,18 @@ +name: Code Coverage + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Upload coverage results + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file From 0c19c53b772ee2b01c19fb751056cb199d774901 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 10:33:39 +0900 Subject: [PATCH 18/30] fix: codecov workflows & requirements.txt --- .github/workflows/coverage.yml | 8 +++++++- requirements.txt | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 53160a0..0c1c778 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,7 +12,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.10 + - name: Upload coverage results uses: codecov/codecov-action@v2 with: - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} + file: .coverage diff --git a/requirements.txt b/requirements.txt index 7b43bd0..c8d95f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ pymongo==4.6.1 pytest==7.4.4 pytest-asyncio==0.23.3 pytest-cov==4.1.0 +pytest-order==1.2.0 python-multipart==0.0.6 sniffio==1.3.0 starlette==0.35.1 From 83e7de4ab01b220d098faf9661927e609ada9b40 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 10:40:28 +0900 Subject: [PATCH 19/30] fix: codecov workflows & requirements.txt --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0c1c778..26d0e4a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,4 +21,4 @@ jobs: uses: codecov/codecov-action@v2 with: token: ${{ secrets.CODECOV_TOKEN }} - file: .coverage + file: ./.coverage From 955b24f2341ac7473bdc58ccc8dab812144143f7 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 10:46:44 +0900 Subject: [PATCH 20/30] fix: conflict main branch --- .github/workflows/coverage.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 99b5e8f..0c1c778 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,8 +21,4 @@ jobs: uses: codecov/codecov-action@v2 with: token: ${{ secrets.CODECOV_TOKEN }} -<<<<<<< HEAD file: .coverage -======= - file: .coverage ->>>>>>> main From 9b5ce3092a6ee43a276c5c13a1e9364b8a7d4710 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 10:48:51 +0900 Subject: [PATCH 21/30] fix: coverage workflow python-version --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0c1c778..8ecc4a5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.10 + python-version: 3.8 - name: Upload coverage results uses: codecov/codecov-action@v2 From 349002c5132f7a9d08169cff28ebb34251af27ed Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 10:52:57 +0900 Subject: [PATCH 22/30] fix: coverage workflow .coverage file path --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8ecc4a5..8ee1854 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,4 +21,4 @@ jobs: uses: codecov/codecov-action@v2 with: token: ${{ secrets.CODECOV_TOKEN }} - file: .coverage + file: ./.coverage From 4daf30faeacdf85f648ddb32d9454a3cf40dfb88 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 11:21:17 +0900 Subject: [PATCH 23/30] fix: coverage workflow to test workflow with ENV --- .coverage | Bin 53248 -> 53248 bytes .github/workflows/coverage.yml | 12 ++++++++++-- .gitignore | 4 +++- requirements.txt | 1 + src/libs/__init__.py | 10 ++-------- src/libs/db_manager.py | 8 +++++--- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/.coverage b/.coverage index eb63146dc08042a3c55c09c04748bd7e809fc152..b5ea4783566ea1a8601043c51344052e5d90df6a 100644 GIT binary patch delta 69 zcmV-L0J{HxpaX!Q1F!~wB1`}e`48?7;}6>p(GSNDyAQ7qr4N}8j}LzjZx3S+S`SRK b5fCH~lS_|{3f~U~1Ox#I4g?Cb None: + def __init__( + self, url: str = os.environ.get('MONGO_CONNECTION_URL') + ) -> None: self.url = url - self.db = conf["mongo"]["DB_NAME"] + self.db = os.environ.get('DB_NAME') def get_session(self) -> pymongo.MongoClient: try: From 24c524d6075dfd2b21dd62dd55e64d75633925cb Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 11:24:51 +0900 Subject: [PATCH 24/30] feature: ADD test_img for testing --- tests/apps/test_controller.py | 2 +- tests/apps/test_img/test.jpg | Bin 0 -> 29474 bytes tests/apps/test_repository.py | 2 +- tests/apps/test_service.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 tests/apps/test_img/test.jpg diff --git a/tests/apps/test_controller.py b/tests/apps/test_controller.py index 3775ba1..1c42986 100644 --- a/tests/apps/test_controller.py +++ b/tests/apps/test_controller.py @@ -8,7 +8,7 @@ # Mock data apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) -img_path = os.path.abspath(os.path.join(apps_path, "img")) +img_path = os.path.abspath(os.path.join(apps_path, "test_img")) USERNAME = "kim" IMAGE_PATH = os.path.abspath(os.path.join(img_path, "test.jpg")) diff --git a/tests/apps/test_img/test.jpg b/tests/apps/test_img/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..98ffc1478b00828c1dc2904e06887ea2d35aae45 GIT binary patch literal 29474 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 W^(&Vk6hC=4c}nUAsdD9l&;Qx3Ob3_% literal 0 HcmV?d00001 diff --git a/tests/apps/test_repository.py b/tests/apps/test_repository.py index ca57b9a..b8e40d8 100644 --- a/tests/apps/test_repository.py +++ b/tests/apps/test_repository.py @@ -8,7 +8,7 @@ apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) -img_path = os.path.abspath(os.path.join(apps_path, "img")) +img_path = os.path.abspath(os.path.join(apps_path, "test_img")) # MOCK data COLLECTION_NAME = "tests" diff --git a/tests/apps/test_service.py b/tests/apps/test_service.py index 6c9312c..9d6d7b9 100644 --- a/tests/apps/test_service.py +++ b/tests/apps/test_service.py @@ -7,7 +7,7 @@ apps_path = os.path.abspath(os.path.join(__file__, os.path.pardir)) -img_path = os.path.abspath(os.path.join(apps_path, "img")) +img_path = os.path.abspath(os.path.join(apps_path, "test_img")) # MOCK data USERNAME = "kim" From 7b1f6d1b6810542e687f49e5786a301d616c34f9 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 12:13:36 +0900 Subject: [PATCH 25/30] fix: coverage workflow env --- .github/workflows/coverage.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a2f028f..c36b6cf 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -25,6 +25,9 @@ jobs: - name: Run tests with coverage run: | pytest --cov-report term --cov=. + env: + MONGO_CONNECTION_URL: ${{ secrets.MONGO_CONNECTION_URL }} + DB_NAME: ${{ secrets.DB_NAME }} - name: Upload coverage results uses: codecov/codecov-action@v2 From 3e49dbd1a72624f8f6a2ef3c35c551547bdde63f Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 12:17:23 +0900 Subject: [PATCH 26/30] test: github env setting check --- .coverage | Bin 53248 -> 53248 bytes src/libs/db_manager.py | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.coverage b/.coverage index b5ea4783566ea1a8601043c51344052e5d90df6a..a932903dc9f6d536440a20d32acd26c15f6b13b5 100644 GIT binary patch delta 22 dcmZozz}&Eac|%=4Bje=y{%FRU&A0ni9ROzO2>}2A delta 22 dcmZozz}&Eac|%=4BlG0?{%A)2&A0ni9ROr=2$lc< diff --git a/src/libs/db_manager.py b/src/libs/db_manager.py index 57e62f2..619c304 100644 --- a/src/libs/db_manager.py +++ b/src/libs/db_manager.py @@ -12,8 +12,11 @@ def __init__( self.db = os.environ.get('DB_NAME') def get_session(self) -> pymongo.MongoClient: + if not self.url: + raise Exception("No url") + try: self.client = pymongo.MongoClient(self.url) return self.client[self.db] except Exception as e: - DBError(**DBErrorCode.DBConnectionError.value, err=e) + raise DBError(**DBErrorCode.DBConnectionError.value, err=e) From 71106990c89b7d0dd1b593e3c17655a74b3d247c Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 12:46:27 +0900 Subject: [PATCH 27/30] fix: coverage workflow create .env File --- .github/workflows/coverage.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c36b6cf..0e695de 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -22,6 +22,11 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt # 프로젝트에 필요한 경우 + - name: Create .env File + run: | + echo "MONGO_CONNECTION_URL=${{ secrets.MONGO_CONNECTION_URL }}" > ./src/libs/.env + echo "DB_NAME=${{ secrets.DB_NAME }}" >> ./src/libs/.env + - name: Run tests with coverage run: | pytest --cov-report term --cov=. From f88d5c6a6ad8be311113c04d06394326627bd3b4 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 12:47:15 +0900 Subject: [PATCH 28/30] fix: coverage workflow create .env File --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0e695de..8c75a78 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,8 +24,8 @@ jobs: - name: Create .env File run: | - echo "MONGO_CONNECTION_URL=${{ secrets.MONGO_CONNECTION_URL }}" > ./src/libs/.env - echo "DB_NAME=${{ secrets.DB_NAME }}" >> ./src/libs/.env + echo "MONGO_CONNECTION_URL=${{ secrets.MONGO_CONNECTION_URL }}" > ./src/libs/.env + echo "DB_NAME=${{ secrets.DB_NAME }}" >> ./src/libs/.env - name: Run tests with coverage run: | From 7d6cc4b28f5a2ee1b9391ec6619549a2b8be9c40 Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 12:52:29 +0900 Subject: [PATCH 29/30] fix: coverage workflow create .env File --- .github/workflows/coverage.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8c75a78..c6e25cb 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,15 +24,14 @@ jobs: - name: Create .env File run: | - echo "MONGO_CONNECTION_URL=${{ secrets.MONGO_CONNECTION_URL }}" > ./src/libs/.env - echo "DB_NAME=${{ secrets.DB_NAME }}" >> ./src/libs/.env + echo ${{ secrets.DB_NAME }} + echo MONGO_CONNECTION_URL=${{ secrets.MONGO_CONNECTION_URL }} > ./src/libs/.env + echo DB_NAME=${{ secrets.DB_NAME }} >> ./src/libs/.env - name: Run tests with coverage run: | pytest --cov-report term --cov=. - env: - MONGO_CONNECTION_URL: ${{ secrets.MONGO_CONNECTION_URL }} - DB_NAME: ${{ secrets.DB_NAME }} + - name: Upload coverage results uses: codecov/codecov-action@v2 From d63e586e591909f53bca43f518e8dbab20d7207b Mon Sep 17 00:00:00 2001 From: robert-min Date: Wed, 24 Jan 2024 12:57:12 +0900 Subject: [PATCH 30/30] fix: coverage workflow create .env File --- .github/workflows/coverage.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c6e25cb..c576abf 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -24,9 +24,12 @@ jobs: - name: Create .env File run: | - echo ${{ secrets.DB_NAME }} - echo MONGO_CONNECTION_URL=${{ secrets.MONGO_CONNECTION_URL }} > ./src/libs/.env - echo DB_NAME=${{ secrets.DB_NAME }} >> ./src/libs/.env + echo $DB_NAME + echo "MONGO_CONNECTION_URL=$MONGO_CONNECTION_URL" > ./src/libs/.env + echo "DB_NAME=$DB_NAME" >> ./src/libs/.env + env: + MONGO_CONNECTION_URL: ${{ secrets.MONGO_CONNECTION_URL }} + DB_NAME: ${{ secrets.DB_NAME }} - name: Run tests with coverage run: |