From e2b743694ac17505fa4ee44616ebcae216cae748 Mon Sep 17 00:00:00 2001 From: Lavitskaya Aleksandra Date: Wed, 4 Jun 2025 02:01:24 +0300 Subject: [PATCH 1/2] new tests --- api_gateway/tests/test_kafka.py | 92 ++++++++++++ docker-compose.yml | 96 ++++++------ e2e_tests/test_full_flow.py | 150 +++++++++++++++++++ proto/statistic_pb2.py | 76 ++++++---- proto/statistic_pb2_grpc.py | 33 ++++ user_service/tests/test_unit_user_service.py | 45 +++++- 6 files changed, 415 insertions(+), 77 deletions(-) create mode 100644 e2e_tests/test_full_flow.py diff --git a/api_gateway/tests/test_kafka.py b/api_gateway/tests/test_kafka.py index 89576c6..97173b3 100644 --- a/api_gateway/tests/test_kafka.py +++ b/api_gateway/tests/test_kafka.py @@ -4,6 +4,9 @@ import time from datetime import datetime import requests +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from statistic_service.db.clickhouse_models import Event, EventType, Base @pytest.fixture(scope="module") @@ -20,6 +23,16 @@ def kafka_consumer(): consumer.close() +@pytest.fixture(scope="module") +def db_session(): + engine = create_engine('clickhouse: // default: password @ clickhouse:8123 / default') + Session = sessionmaker(bind=engine) + Base.metadata.create_all(engine) + session = Session() + yield session + session.close() + + @pytest.fixture(scope="module") def test_user(): return { @@ -33,6 +46,7 @@ def test_user(): def test_user_registration_event(kafka_consumer, test_user): kafka_consumer.subscribe(['user_registrations']) + response = requests.post( 'http://api_gateway:8080/api/v1/register', json=test_user @@ -58,6 +72,84 @@ def test_user_registration_event(kafka_consumer, test_user): pytest.fail("Event not received in Kafka within timeout") +def test_post_like_event_reading(kafka_consumer, test_user): + login_response = requests.post( + 'http://api_gateway:8080/api/v1/login', + json={"login": test_user["login"], "password": test_user["password"]} + ) + assert login_response.status_code == 200 + token = login_response.json()["token"] + + post_response = requests.post( + 'http://api_gateway:8080/api/v1/posts', + headers={"Authorization": token}, + json={"title": "Test Post", "description": "Test", "is_private": False} + ) + assert post_response.status_code == 201 + post_id = post_response.json()["post_id"] + + kafka_consumer.subscribe(['post_likes']) + like_response = requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_id}/like', + headers={"Authorization": token} + ) + assert like_response.status_code == 200 + + start_time = time.time() + while time.time() - start_time < 10: + msg = kafka_consumer.poll(1.0) + if msg is None: + continue + if msg.error(): + raise KafkaException(msg.error()) + + event = json.loads(msg.value()) + if event.get("post_id") == str(post_id): + assert event["event_type"] == "post_liked" + assert event["user_id"] + assert msg.key() == str(post_id).encode('utf-8') + return + + pytest.fail("Event not received in Kafka within timeout") + + +def test_post_view_event_saved_to_db(db_session, test_user): + login_response = requests.post( + 'http://api_gateway:8080/api/v1/login', + json={ + "login": test_user["login"], + "password": test_user["password"] + } + ) + assert login_response.status_code == 200 + token = login_response.json()["token"] + + post_response = requests.post( + 'http://api_gateway:8080/api/v1/posts', + headers={"Authorization": token}, + json={ + "title": "Kafka Test Post", + "description": "Post for Kafka testing", + "is_private": False + } + ) + assert post_response.status_code == 201 + post_id = post_response.json()["post_id"] + + view_response = requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_id}/view', + headers={"Authorization": token} + ) + assert view_response.status_code == 200 + + time.sleep(5) + + event = db_session.query(Event).filter_by(post_id=post_id, event_type=EventType.VIEW).first() + assert event is not None + assert event.post_id == post_id + assert event.event_type == EventType.VIEW + + def test_post_view_event(kafka_consumer, test_user): login_response = requests.post( 'http://api_gateway:8080/api/v1/login', diff --git a/docker-compose.yml b/docker-compose.yml index 9c38020..982fb53 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,54 +65,54 @@ services: timeout: 5s retries: 10 - # tests: - # build: - # context: . - # dockerfile: api_gateway/Dockerfile - # volumes: - # - ./api_gateway:/app/api_gateway - # - ./proto:/app/proto - # - ./user_service:/app/user_service - # - ./post_service:/app/post_service - # - ./broker:/app/broker - # command: > - # sh -c " - # echo 'Ожидание готовности сервисов...' && - # while ! nc -z user_service 5000; do sleep 2; done && - # while ! nc -z post_service 50051; do sleep 2; done && - # while ! curl -f http://user_service:5000/health; do sleep 2; done && - # while ! nc -z kafka 9092; do sleep 2; done && - # - # export PYTHONPATH=/app:/app/user_service:/app/post_service:/app/api_gateway:/app/broker:/app/proto && - # - # echo 'Запуск тестов api_gateway...' && - # cd /app && - # pytest api_gateway/tests/test_user.py api_gateway/tests/test_post.py api_gateway/tests/test_kafka.py -v || exit 1 && - # - # echo 'Запуск тестов user_service...' && - # cd /app/user_service && - # pytest tests/test_unit_models.py tests/test_unit_user_service.py tests/test_unit_validators.py -v || exit 1 && - # - # echo 'Запуск тестов post_service...' && - # cd /app/post_service && - # pytest tests/test_unit_models.py tests/test_unit_post_service.py tests/test_unit_kafka_events.py -v || exit 1 - # " - # depends_on: - # - api_gateway - # - user_service - # - post_service - # - db - # - kafka - # networks: - # - social-network - # environment: - # - PYTHONPATH=/app:/app/user_service:/app/post_service:/app/api_gateway:/app/broker:/app/proto - # - FLASK_ENV=testing - # - TZ=Europe/Moscow - # - POST_SERVICE_HOST=post_service - # - POST_SERVICE_PORT=50051 - # - USER_SERVICE_URL=http://user_service:5000 - # - KAFKA_BOOTSTRAP_SERVERS=kafka:9092 + tests: + build: + context: . + dockerfile: api_gateway/Dockerfile + volumes: + - ./api_gateway:/app/api_gateway + - ./proto:/app/proto + - ./user_service:/app/user_service + - ./post_service:/app/post_service + - ./broker:/app/broker + command: > + sh -c " + echo 'Ожидание готовности сервисов...' && + while ! nc -z user_service 5000; do sleep 2; done && + while ! nc -z post_service 50051; do sleep 2; done && + while ! curl -f http://user_service:5000/health; do sleep 2; done && + while ! nc -z kafka 9092; do sleep 2; done && + + export PYTHONPATH=/app:/app/user_service:/app/post_service:/app/api_gateway:/app/broker:/app/proto && + + echo 'Запуск тестов api_gateway...' && + cd /app && + pytest api_gateway/tests/test_user.py api_gateway/tests/test_post.py api_gateway/tests/test_kafka.py -v || exit 1 && + + echo 'Запуск тестов user_service...' && + cd /app/user_service && + pytest tests/test_unit_models.py tests/test_unit_user_service.py tests/test_unit_validators.py -v || exit 1 && + + echo 'Запуск тестов post_service...' && + cd /app/post_service && + pytest tests/test_unit_models.py tests/test_unit_post_service.py tests/test_unit_kafka_events.py -v || exit 1 + " + depends_on: + - api_gateway + - user_service + - post_service + - db + - kafka + networks: + - social-network + environment: + - PYTHONPATH=/app:/app/user_service:/app/post_service:/app/api_gateway:/app/broker:/app/proto + - FLASK_ENV=testing + - TZ=Europe/Moscow + - POST_SERVICE_HOST=post_service + - POST_SERVICE_PORT=50051 + - USER_SERVICE_URL=http://user_service:5000 + - KAFKA_BOOTSTRAP_SERVERS=kafka:9092 zookeeper: image: confluentinc/cp-zookeeper:7.3.0 diff --git a/e2e_tests/test_full_flow.py b/e2e_tests/test_full_flow.py new file mode 100644 index 0000000..01b0027 --- /dev/null +++ b/e2e_tests/test_full_flow.py @@ -0,0 +1,150 @@ +import pytest +import requests +import time + + +@pytest.fixture(scope="module") +def test_user(): + return { + "login": "e2e_test_user", + "password": "E2eTest123!", + "email": "e2e.test@example.com", + "first_name": "EndToEnd", + "last_name": "Test" + } + + +@pytest.fixture(scope="function") +def registered_user(test_user): + requests.post('http://api_gateway:8080/api/v1/register', json=test_user) + return test_user + + +@pytest.fixture(scope="function") +def auth_token(registered_user): + login_response = requests.post( + 'http://api_gateway:8080/api/v1/login', + json={"login": registered_user["login"], "password": registered_user["password"]} + ) + return login_response.json()["token"] + + +def test_full_flow_post_creation_view_and_stats(auth_token): + post_data = { + "title": "E2E Test Post", + "description": "Post for end-to-end testing", + "is_private": False + } + post_response = requests.post( + 'http://api_gateway:8080/api/v1/posts', + headers={"Authorization": auth_token}, + json=post_data + ) + post_id = post_response.json()["post_id"] + + for _ in range(3): + requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_id}/view', + headers={"Authorization": auth_token} + ) + + time.sleep(5) + + stats_response = requests.get( + f'http://api_gateway:8080/api/v1/posts/{post_id}/stats', + headers={"Authorization": auth_token} + ) + stats = stats_response.json() + assert stats["views_count"] >= 3 + + +def test_post_likes_and_comments_flow(auth_token): + post_response = requests.post( + 'http://api_gateway:8080/api/v1/posts', + headers={"Authorization": auth_token}, + json={ + "title": "Likes and Comments Test", + "description": "Testing likes and comments flow", + "is_private": False + } + ) + post_id = post_response.json()["post_id"] + + for _ in range(2): + requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_id}/like', + headers={"Authorization": auth_token} + ) + + comment_text = "This is a test comment for E2E testing" + requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_id}/comment', + headers={"Authorization": auth_token}, + json={"text": comment_text} + ) + + time.sleep(5) + + stats_response = requests.get( + f'http://api_gateway:8080/api/v1/posts/{post_id}/stats', + headers={"Authorization": auth_token} + ) + stats = stats_response.json() + assert stats["likes_count"] >= 2 + assert stats["comments_count"] >= 1 + + +def test_user_profile_and_post_dynamics(auth_token): + update_data = { + "first_name": "UpdatedFirstName", + "profile": { + "city": "Test City", + "about_me": "E2E Test User" + } + } + requests.put( + 'http://api_gateway:8080/api/v1/profile', + headers={"Authorization": auth_token}, + json=update_data + ) + + post_ids = [] + for i in range(2): + post_response = requests.post( + 'http://api_gateway:8080/api/v1/posts', + headers={"Authorization": auth_token}, + json={ + "title": f"Dynamics Test Post {i + 1}", + "description": f"Post {i + 1} for dynamics testing", + "is_private": False + } + ) + post_ids.append(post_response.json()["post_id"]) + + for _ in range(5): + requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_ids[0]}/view', + headers={"Authorization": auth_token} + ) + + for _ in range(3): + requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_ids[1]}/like', + headers={"Authorization": auth_token} + ) + + time.sleep(10) + + dynamics_response = requests.get( + f'http://api_gateway:8080/api/v1/posts/{post_ids[0]}/dynamic?metric=views', + headers={"Authorization": auth_token} + ) + dynamics = dynamics_response.json() + assert any(d["count"] >= 5 for d in dynamics) + + top_posts_response = requests.get( + 'http://api_gateway:8080/api/v1/posts/top?metric=views', + headers={"Authorization": auth_token} + ) + top_posts = top_posts_response.json() + assert any(p["post_id"] == post_ids[0] for p in top_posts) diff --git a/proto/statistic_pb2.py b/proto/statistic_pb2.py index 7f42a28..c312262 100644 --- a/proto/statistic_pb2.py +++ b/proto/statistic_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15proto/statistic.proto\x12\tstatistic\"4\n\x10PostStatsRequest\x12\x0f\n\x07post_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\"i\n\x11PostStatsResponse\x12\x13\n\x0bviews_count\x18\x01 \x01(\x04\x12\x13\n\x0blikes_count\x18\x02 \x01(\x04\x12\x16\n\x0e\x63omments_count\x18\x03 \x01(\x04\x12\x12\n\nupdated_at\x18\x04 \x01(\t\"\x9a\x01\n\x12PostDynamicRequest\x12\x0f\n\x07post_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12\x34\n\x06metric\x18\x03 \x01(\x0e\x32$.statistic.PostDynamicRequest.Metric\",\n\x06Metric\x12\t\n\x05VIEWS\x10\x00\x12\t\n\x05LIKES\x10\x01\x12\x0c\n\x08\x43OMMENTS\x10\x02\"(\n\tDailyStat\x12\x0c\n\x04\x64\x61te\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\":\n\x13PostDynamicResponse\x12#\n\x05stats\x18\x01 \x03(\x0b\x32\x14.statistic.DailyStat\"r\n\x0fTopPostsRequest\x12\x31\n\x06metric\x18\x01 \x01(\x0e\x32!.statistic.TopPostsRequest.Metric\",\n\x06Metric\x12\t\n\x05VIEWS\x10\x00\x12\t\n\x05LIKES\x10\x01\x12\x0c\n\x08\x43OMMENTS\x10\x02\")\n\x07TopPost\x12\x0f\n\x07post_id\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"5\n\x10TopPostsResponse\x12!\n\x05posts\x18\x01 \x03(\x0b\x32\x12.statistic.TopPost\"r\n\x0fTopUsersRequest\x12\x31\n\x06metric\x18\x01 \x01(\x0e\x32!.statistic.TopUsersRequest.Metric\",\n\x06Metric\x12\t\n\x05VIEWS\x10\x00\x12\t\n\x05LIKES\x10\x01\x12\x0c\n\x08\x43OMMENTS\x10\x02\")\n\x07TopUser\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"5\n\x10TopUsersResponse\x12!\n\x05users\x18\x01 \x03(\x0b\x32\x12.statistic.TopUser2\xc6\x02\n\x10StatisticService\x12K\n\x0cGetPostStats\x12\x1b.statistic.PostStatsRequest\x1a\x1c.statistic.PostStatsResponse\"\x00\x12Q\n\x0eGetPostDynamic\x12\x1d.statistic.PostDynamicRequest\x1a\x1e.statistic.PostDynamicResponse\"\x00\x12H\n\x0bGetTopPosts\x12\x1a.statistic.TopPostsRequest\x1a\x1b.statistic.TopPostsResponse\"\x00\x12H\n\x0bGetTopUsers\x12\x1a.statistic.TopUsersRequest\x1a\x1b.statistic.TopUsersResponse\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15proto/statistic.proto\x12\tstatistic\"4\n\x10PostStatsRequest\x12\x0f\n\x07post_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\"U\n\x11PostStatsResponse\x12\x13\n\x0bviews_count\x18\x01 \x01(\x04\x12\x13\n\x0blikes_count\x18\x02 \x01(\x04\x12\x16\n\x0e\x63omments_count\x18\x03 \x01(\x04\"\x9a\x01\n\x12PostDynamicRequest\x12\x0f\n\x07post_id\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\x12\x34\n\x06metric\x18\x03 \x01(\x0e\x32$.statistic.PostDynamicRequest.Metric\",\n\x06Metric\x12\t\n\x05VIEWS\x10\x00\x12\t\n\x05LIKES\x10\x01\x12\x0c\n\x08\x43OMMENTS\x10\x02\"(\n\tDailyStat\x12\x0c\n\x04\x64\x61te\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\":\n\x13PostDynamicResponse\x12#\n\x05stats\x18\x01 \x03(\x0b\x32\x14.statistic.DailyStat\"\x83\x01\n\x0fTopPostsRequest\x12\x31\n\x06metric\x18\x01 \x01(\x0e\x32!.statistic.TopPostsRequest.Metric\x12\x0f\n\x07user_id\x18\x02 \x01(\t\",\n\x06Metric\x12\t\n\x05VIEWS\x10\x00\x12\t\n\x05LIKES\x10\x01\x12\x0c\n\x08\x43OMMENTS\x10\x02\")\n\x07TopPost\x12\x0f\n\x07post_id\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"5\n\x10TopPostsResponse\x12!\n\x05posts\x18\x01 \x03(\x0b\x32\x12.statistic.TopPost\"\x83\x01\n\x0fTopUsersRequest\x12\x31\n\x06metric\x18\x01 \x01(\x0e\x32!.statistic.TopUsersRequest.Metric\x12\x0f\n\x07user_id\x18\x02 \x01(\t\",\n\x06Metric\x12\t\n\x05VIEWS\x10\x00\x12\t\n\x05LIKES\x10\x01\x12\x0c\n\x08\x43OMMENTS\x10\x02\")\n\x07TopUser\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\r\n\x05\x63ount\x18\x02 \x01(\x04\"5\n\x10TopUsersResponse\x12!\n\x05users\x18\x01 \x03(\x0b\x32\x12.statistic.TopUser\"\x13\n\x11GetPostIdsRequest\"&\n\x12GetPostIdsResponse\x12\x10\n\x08post_ids\x18\x01 \x03(\t2\x93\x03\n\x10StatisticService\x12K\n\x0cGetPostStats\x12\x1b.statistic.PostStatsRequest\x1a\x1c.statistic.PostStatsResponse\"\x00\x12Q\n\x0eGetPostDynamic\x12\x1d.statistic.PostDynamicRequest\x1a\x1e.statistic.PostDynamicResponse\"\x00\x12H\n\x0bGetTopPosts\x12\x1a.statistic.TopPostsRequest\x1a\x1b.statistic.TopPostsResponse\"\x00\x12H\n\x0bGetTopUsers\x12\x1a.statistic.TopUsersRequest\x1a\x1b.statistic.TopUsersResponse\"\x00\x12K\n\nGetPostIds\x12\x1c.statistic.GetPostIdsRequest\x1a\x1d.statistic.GetPostIdsResponse\"\x00\x62\x06proto3') @@ -29,6 +29,8 @@ _TOPUSERSREQUEST = DESCRIPTOR.message_types_by_name['TopUsersRequest'] _TOPUSER = DESCRIPTOR.message_types_by_name['TopUser'] _TOPUSERSRESPONSE = DESCRIPTOR.message_types_by_name['TopUsersResponse'] +_GETPOSTIDSREQUEST = DESCRIPTOR.message_types_by_name['GetPostIdsRequest'] +_GETPOSTIDSRESPONSE = DESCRIPTOR.message_types_by_name['GetPostIdsResponse'] _POSTDYNAMICREQUEST_METRIC = _POSTDYNAMICREQUEST.enum_types_by_name['Metric'] _TOPPOSTSREQUEST_METRIC = _TOPPOSTSREQUEST.enum_types_by_name['Metric'] _TOPUSERSREQUEST_METRIC = _TOPUSERSREQUEST.enum_types_by_name['Metric'] @@ -109,6 +111,20 @@ }) _sym_db.RegisterMessage(TopUsersResponse) +GetPostIdsRequest = _reflection.GeneratedProtocolMessageType('GetPostIdsRequest', (_message.Message,), { + 'DESCRIPTOR' : _GETPOSTIDSREQUEST, + '__module__' : 'proto.statistic_pb2' + # @@protoc_insertion_point(class_scope:statistic.GetPostIdsRequest) + }) +_sym_db.RegisterMessage(GetPostIdsRequest) + +GetPostIdsResponse = _reflection.GeneratedProtocolMessageType('GetPostIdsResponse', (_message.Message,), { + 'DESCRIPTOR' : _GETPOSTIDSRESPONSE, + '__module__' : 'proto.statistic_pb2' + # @@protoc_insertion_point(class_scope:statistic.GetPostIdsResponse) + }) +_sym_db.RegisterMessage(GetPostIdsResponse) + _STATISTICSERVICE = DESCRIPTOR.services_by_name['StatisticService'] if _descriptor._USE_C_DESCRIPTORS == False: @@ -116,31 +132,35 @@ _POSTSTATSREQUEST._serialized_start=36 _POSTSTATSREQUEST._serialized_end=88 _POSTSTATSRESPONSE._serialized_start=90 - _POSTSTATSRESPONSE._serialized_end=195 - _POSTDYNAMICREQUEST._serialized_start=198 - _POSTDYNAMICREQUEST._serialized_end=352 - _POSTDYNAMICREQUEST_METRIC._serialized_start=308 - _POSTDYNAMICREQUEST_METRIC._serialized_end=352 - _DAILYSTAT._serialized_start=354 - _DAILYSTAT._serialized_end=394 - _POSTDYNAMICRESPONSE._serialized_start=396 - _POSTDYNAMICRESPONSE._serialized_end=454 - _TOPPOSTSREQUEST._serialized_start=456 - _TOPPOSTSREQUEST._serialized_end=570 - _TOPPOSTSREQUEST_METRIC._serialized_start=308 - _TOPPOSTSREQUEST_METRIC._serialized_end=352 - _TOPPOST._serialized_start=572 - _TOPPOST._serialized_end=613 - _TOPPOSTSRESPONSE._serialized_start=615 - _TOPPOSTSRESPONSE._serialized_end=668 - _TOPUSERSREQUEST._serialized_start=670 - _TOPUSERSREQUEST._serialized_end=784 - _TOPUSERSREQUEST_METRIC._serialized_start=308 - _TOPUSERSREQUEST_METRIC._serialized_end=352 - _TOPUSER._serialized_start=786 - _TOPUSER._serialized_end=827 - _TOPUSERSRESPONSE._serialized_start=829 - _TOPUSERSRESPONSE._serialized_end=882 - _STATISTICSERVICE._serialized_start=885 - _STATISTICSERVICE._serialized_end=1211 + _POSTSTATSRESPONSE._serialized_end=175 + _POSTDYNAMICREQUEST._serialized_start=178 + _POSTDYNAMICREQUEST._serialized_end=332 + _POSTDYNAMICREQUEST_METRIC._serialized_start=288 + _POSTDYNAMICREQUEST_METRIC._serialized_end=332 + _DAILYSTAT._serialized_start=334 + _DAILYSTAT._serialized_end=374 + _POSTDYNAMICRESPONSE._serialized_start=376 + _POSTDYNAMICRESPONSE._serialized_end=434 + _TOPPOSTSREQUEST._serialized_start=437 + _TOPPOSTSREQUEST._serialized_end=568 + _TOPPOSTSREQUEST_METRIC._serialized_start=288 + _TOPPOSTSREQUEST_METRIC._serialized_end=332 + _TOPPOST._serialized_start=570 + _TOPPOST._serialized_end=611 + _TOPPOSTSRESPONSE._serialized_start=613 + _TOPPOSTSRESPONSE._serialized_end=666 + _TOPUSERSREQUEST._serialized_start=669 + _TOPUSERSREQUEST._serialized_end=800 + _TOPUSERSREQUEST_METRIC._serialized_start=288 + _TOPUSERSREQUEST_METRIC._serialized_end=332 + _TOPUSER._serialized_start=802 + _TOPUSER._serialized_end=843 + _TOPUSERSRESPONSE._serialized_start=845 + _TOPUSERSRESPONSE._serialized_end=898 + _GETPOSTIDSREQUEST._serialized_start=900 + _GETPOSTIDSREQUEST._serialized_end=919 + _GETPOSTIDSRESPONSE._serialized_start=921 + _GETPOSTIDSRESPONSE._serialized_end=959 + _STATISTICSERVICE._serialized_start=962 + _STATISTICSERVICE._serialized_end=1365 # @@protoc_insertion_point(module_scope) diff --git a/proto/statistic_pb2_grpc.py b/proto/statistic_pb2_grpc.py index 7b0217a..305645c 100644 --- a/proto/statistic_pb2_grpc.py +++ b/proto/statistic_pb2_grpc.py @@ -34,6 +34,11 @@ def __init__(self, channel): request_serializer=proto_dot_statistic__pb2.TopUsersRequest.SerializeToString, response_deserializer=proto_dot_statistic__pb2.TopUsersResponse.FromString, ) + self.GetPostIds = channel.unary_unary( + '/statistic.StatisticService/GetPostIds', + request_serializer=proto_dot_statistic__pb2.GetPostIdsRequest.SerializeToString, + response_deserializer=proto_dot_statistic__pb2.GetPostIdsResponse.FromString, + ) class StatisticServiceServicer(object): @@ -63,6 +68,12 @@ def GetTopUsers(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetPostIds(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_StatisticServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -86,6 +97,11 @@ def add_StatisticServiceServicer_to_server(servicer, server): request_deserializer=proto_dot_statistic__pb2.TopUsersRequest.FromString, response_serializer=proto_dot_statistic__pb2.TopUsersResponse.SerializeToString, ), + 'GetPostIds': grpc.unary_unary_rpc_method_handler( + servicer.GetPostIds, + request_deserializer=proto_dot_statistic__pb2.GetPostIdsRequest.FromString, + response_serializer=proto_dot_statistic__pb2.GetPostIdsResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'statistic.StatisticService', rpc_method_handlers) @@ -163,3 +179,20 @@ def GetTopUsers(request, proto_dot_statistic__pb2.TopUsersResponse.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetPostIds(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/statistic.StatisticService/GetPostIds', + proto_dot_statistic__pb2.GetPostIdsRequest.SerializeToString, + proto_dot_statistic__pb2.GetPostIdsResponse.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/user_service/tests/test_unit_user_service.py b/user_service/tests/test_unit_user_service.py index 54202a6..63eef70 100644 --- a/user_service/tests/test_unit_user_service.py +++ b/user_service/tests/test_unit_user_service.py @@ -2,7 +2,10 @@ import pytest import datetime import jwt -from ..user_service import app, User, generate_jwt, decode_jwt +from user_service.user_service import app, User, UserProfile, generate_jwt, decode_jwt +from werkzeug.security import generate_password_hash, check_password_hash +import pytest +from unittest.mock import patch, MagicMock @pytest.fixture @@ -84,6 +87,26 @@ def test_register_user_duplicate_login(mock_db): assert response.json == {"message": "Login is already taken."} +def test_login_user_success(mock_db): + mock_user = MagicMock() + mock_user.first.return_value = User( + user_id="123", + login="test_user", + hashed_password=generate_password_hash("P@ssword123", method='scrypt'), + is_active=True + ) + mock_db["mock_user_query"].filter_by.return_value = mock_user + + response = app.test_client().post('/login', json={ + "login": "test_user", + "password": "P@ssword123" + }) + + assert response.status_code == 200 + assert "token" in response.json + mock_db["mock_user_query"].filter_by.assert_called_once_with(login="test_user") + + def test_login_user_invalid_password(mock_db): mock_user = MagicMock() mock_user.first.return_value = User( @@ -116,6 +139,26 @@ def test_login_user_not_found(mock_db): assert response.json == {"message": "Invalid credentials."} +def test_update_profile_saves_to_db(mock_db): + mock_user = MagicMock() + mock_user.profile = UserProfile(profile_id="456", user_id="123") + mock_db["mock_user_query"].get.return_value = mock_user + + with patch('user_service.decode_jwt', return_value={"user_id": "123"}): + response = app.test_client().put('/profile', + headers={"Authorization": "valid_token"}, + json={ + "first_name": "NewName", + "profile": {"city": "Moscow"} + } + ) + + assert response.status_code == 200 + assert mock_user.first_name == "NewName" + assert mock_user.profile.city == "Moscow" + mock_db["mock_commit"].assert_called_once() + + @patch('app.user_service.user_service.jwt.encode') def test_generate_jwt(mock_encode): mock_encode.return_value = "test_token" From d43632c8f0ba9b96b1ff2833a342f32c45e68c3f Mon Sep 17 00:00:00 2001 From: Lavitskaya Aleksandra Date: Wed, 4 Jun 2025 17:28:23 +0300 Subject: [PATCH 2/2] tests improved --- api_gateway/requirements.txt | 3 +- api_gateway/tests/test_kafka.py | 185 +++++-------------- docker-compose.yml | 32 +++- e2e_tests/test_full_flow.py | 65 +++++-- post_service/tests/test_unit_post_service.py | 31 ++++ user_service/tests/test_unit_user_service.py | 85 +++++---- user_service/user_service.py | 2 +- 7 files changed, 204 insertions(+), 199 deletions(-) diff --git a/api_gateway/requirements.txt b/api_gateway/requirements.txt index 6aa015a..61037fb 100644 --- a/api_gateway/requirements.txt +++ b/api_gateway/requirements.txt @@ -15,4 +15,5 @@ pytest-cov pytest-mock>=3.0.0 pytest-dependency coverage -confluent-kafka==2.2.0 \ No newline at end of file +confluent-kafka==2.2.0 +clickhouse-sqlalchemy diff --git a/api_gateway/tests/test_kafka.py b/api_gateway/tests/test_kafka.py index 97173b3..29cbfa7 100644 --- a/api_gateway/tests/test_kafka.py +++ b/api_gateway/tests/test_kafka.py @@ -2,11 +2,18 @@ import json from confluent_kafka import Consumer, KafkaException import time -from datetime import datetime import requests -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -from statistic_service.db.clickhouse_models import Event, EventType, Base +from sqlalchemy import create_engine, select +from clickhouse_sqlalchemy import make_session +from statistic_service.db.clickhouse_models import Event, EventType + + +@pytest.fixture(scope="module") +def clickhouse_session(): + engine = create_engine('clickhouse://default:password@clickhouse:8123/default') + session = make_session(engine) + yield session + session.close() @pytest.fixture(scope="module") @@ -23,16 +30,6 @@ def kafka_consumer(): consumer.close() -@pytest.fixture(scope="module") -def db_session(): - engine = create_engine('clickhouse: // default: password @ clickhouse:8123 / default') - Session = sessionmaker(bind=engine) - Base.metadata.create_all(engine) - session = Session() - yield session - session.close() - - @pytest.fixture(scope="module") def test_user(): return { @@ -46,7 +43,6 @@ def test_user(): def test_user_registration_event(kafka_consumer, test_user): kafka_consumer.subscribe(['user_registrations']) - response = requests.post( 'http://api_gateway:8080/api/v1/register', json=test_user @@ -72,7 +68,7 @@ def test_user_registration_event(kafka_consumer, test_user): pytest.fail("Event not received in Kafka within timeout") -def test_post_like_event_reading(kafka_consumer, test_user): +def test_post_view_event(kafka_consumer, test_user, clickhouse_session): login_response = requests.post( 'http://api_gateway:8080/api/v1/login', json={"login": test_user["login"], "password": test_user["password"]} @@ -83,96 +79,13 @@ def test_post_like_event_reading(kafka_consumer, test_user): post_response = requests.post( 'http://api_gateway:8080/api/v1/posts', headers={"Authorization": token}, - json={"title": "Test Post", "description": "Test", "is_private": False} + json={"title": "Kafka Test Post", "description": "Post for Kafka testing", "is_private": False} ) assert post_response.status_code == 201 post_id = post_response.json()["post_id"] - kafka_consumer.subscribe(['post_likes']) - like_response = requests.post( - f'http://api_gateway:8080/api/v1/posts/{post_id}/like', - headers={"Authorization": token} - ) - assert like_response.status_code == 200 - - start_time = time.time() - while time.time() - start_time < 10: - msg = kafka_consumer.poll(1.0) - if msg is None: - continue - if msg.error(): - raise KafkaException(msg.error()) - - event = json.loads(msg.value()) - if event.get("post_id") == str(post_id): - assert event["event_type"] == "post_liked" - assert event["user_id"] - assert msg.key() == str(post_id).encode('utf-8') - return - - pytest.fail("Event not received in Kafka within timeout") - - -def test_post_view_event_saved_to_db(db_session, test_user): - login_response = requests.post( - 'http://api_gateway:8080/api/v1/login', - json={ - "login": test_user["login"], - "password": test_user["password"] - } - ) - assert login_response.status_code == 200 - token = login_response.json()["token"] - - post_response = requests.post( - 'http://api_gateway:8080/api/v1/posts', - headers={"Authorization": token}, - json={ - "title": "Kafka Test Post", - "description": "Post for Kafka testing", - "is_private": False - } - ) - assert post_response.status_code == 201 - post_id = post_response.json()["post_id"] - - view_response = requests.post( - f'http://api_gateway:8080/api/v1/posts/{post_id}/view', - headers={"Authorization": token} - ) - assert view_response.status_code == 200 - - time.sleep(5) - - event = db_session.query(Event).filter_by(post_id=post_id, event_type=EventType.VIEW).first() - assert event is not None - assert event.post_id == post_id - assert event.event_type == EventType.VIEW - - -def test_post_view_event(kafka_consumer, test_user): - login_response = requests.post( - 'http://api_gateway:8080/api/v1/login', - json={ - "login": test_user["login"], - "password": test_user["password"] - } - ) - assert login_response.status_code == 200 - token = login_response.json()["token"] - - post_response = requests.post( - 'http://api_gateway:8080/api/v1/posts', - headers={"Authorization": token}, - json={ - "title": "Kafka Test Post", - "description": "Post for Kafka testing", - "is_private": False - } - ) - assert post_response.status_code == 201 - post_id = post_response.json()["post_id"] kafka_consumer.subscribe(['post_views']) + view_response = requests.post( f'http://api_gateway:8080/api/v1/posts/{post_id}/view', headers={"Authorization": token} @@ -190,22 +103,22 @@ def test_post_view_event(kafka_consumer, test_user): event = json.loads(msg.value()) if event.get("post_id") == str(post_id): assert event['event_type'] == 'post_viewed' - assert 'user_id' in event - assert 'post_id' in event - assert datetime.fromisoformat(event['timestamp']).tzinfo is None - assert msg.key() == str(post_id).encode('utf-8') + time.sleep(1) + stmt = select(Event).where( + Event.post_id == str(post_id), + Event.event_type == EventType.VIEW + ) + result = clickhouse_session.execute(stmt).fetchone() + assert result is not None, "Event not found in ClickHouse" return - pytest.fail("Event not received in Kafka within timeout") + pytest.fail("Kafka event not received within timeout") -def test_post_like_event(kafka_consumer, test_user): +def test_post_like_event(kafka_consumer, test_user, clickhouse_session): login_response = requests.post( 'http://api_gateway:8080/api/v1/login', - json={ - "login": test_user["login"], - "password": test_user["password"] - } + json={"login": test_user["login"], "password": test_user["password"]} ) assert login_response.status_code == 200 token = login_response.json()["token"] @@ -213,15 +126,13 @@ def test_post_like_event(kafka_consumer, test_user): post_response = requests.post( 'http://api_gateway:8080/api/v1/posts', headers={"Authorization": token}, - json={ - "title": "Kafka Like Test", - "description": "Post for like testing", - "is_private": False - } + json={"title": "Kafka Like Test", "description": "Post for like testing", "is_private": False} ) assert post_response.status_code == 201 post_id = post_response.json()["post_id"] + kafka_consumer.subscribe(['post_likes']) + like_response = requests.post( f'http://api_gateway:8080/api/v1/posts/{post_id}/like', headers={"Authorization": token} @@ -239,22 +150,22 @@ def test_post_like_event(kafka_consumer, test_user): event = json.loads(msg.value()) if event.get("post_id") == str(post_id): assert event['event_type'] == 'post_liked' - assert 'user_id' in event - assert 'post_id' in event - assert datetime.fromisoformat(event['timestamp']).tzinfo is None - assert msg.key() == str(post_id).encode('utf-8') + time.sleep(1) + stmt = select(Event).where( + Event.post_id == str(post_id), + Event.event_type == EventType.LIKE + ) + result = clickhouse_session.execute(stmt).fetchone() + assert result is not None, "Like event not found in ClickHouse" return - pytest.fail("Event not received in Kafka within timeout") + pytest.fail("Kafka event not received within timeout") -def test_post_comment_event(kafka_consumer, test_user): +def test_post_comment_event(kafka_consumer, test_user, clickhouse_session): login_response = requests.post( 'http://api_gateway:8080/api/v1/login', - json={ - "login": test_user["login"], - "password": test_user["password"] - } + json={"login": test_user["login"], "password": test_user["password"]} ) assert login_response.status_code == 200 token = login_response.json()["token"] @@ -262,16 +173,13 @@ def test_post_comment_event(kafka_consumer, test_user): post_response = requests.post( 'http://api_gateway:8080/api/v1/posts', headers={"Authorization": token}, - json={ - "title": "Kafka Comment Test", - "description": "Post for comment testing", - "is_private": False - } + json={"title": "Kafka Comment Test", "description": "Post for comment testing", "is_private": False} ) assert post_response.status_code == 201 post_id = post_response.json()["post_id"] kafka_consumer.subscribe(['post_comments']) + comment_text = "Test comment for Kafka" comment_response = requests.post( f'http://api_gateway:8080/api/v1/posts/{post_id}/comment', @@ -292,12 +200,13 @@ def test_post_comment_event(kafka_consumer, test_user): event = json.loads(msg.value()) if event.get("post_id") == str(post_id) and event.get("comment_id") == str(comment_id): assert event['event_type'] == 'post_commented' - assert 'user_id' in event - assert 'post_id' in event - assert 'comment_id' in event - assert datetime.fromisoformat(event['timestamp']).tzinfo is None - assert msg.key() == str(post_id).encode('utf-8') - assert event.get("text_preview") == comment_text[:100] + time.sleep(1) + stmt = select(Event).where( + Event.post_id == str(post_id), + Event.event_type == EventType.COMMENT + ) + result = clickhouse_session.execute(stmt).fetchone() + assert result is not None, "Comment event not found in ClickHouse" return - pytest.fail("Event not received in Kafka within timeout") + pytest.fail("Kafka event not received within timeout") diff --git a/docker-compose.yml b/docker-compose.yml index 982fb53..605007b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: environment: - POST_SERVICE_HOST=post_service - POST_SERVICE_PORT=50051 + - USER_SERVICE_URL=http://user_service:5000 - PYTHONPATH=/app:/app/proto user_service: @@ -74,7 +75,9 @@ services: - ./proto:/app/proto - ./user_service:/app/user_service - ./post_service:/app/post_service + - ./statistic_service:/app/statistic_service - ./broker:/app/broker + - ./e2e_tests:/app/e2e_tests command: > sh -c " echo 'Ожидание готовности сервисов...' && @@ -82,20 +85,29 @@ services: while ! nc -z post_service 50051; do sleep 2; done && while ! curl -f http://user_service:5000/health; do sleep 2; done && while ! nc -z kafka 9092; do sleep 2; done && - - export PYTHONPATH=/app:/app/user_service:/app/post_service:/app/api_gateway:/app/broker:/app/proto && - - echo 'Запуск тестов api_gateway...' && - cd /app && - pytest api_gateway/tests/test_user.py api_gateway/tests/test_post.py api_gateway/tests/test_kafka.py -v || exit 1 && - + while ! nc -z kafka 9092; do sleep 2; done && + + export PYTHONPATH=/app:/app/user_service:/app/post_service:/app/api_gateway:/app/broker:/app/proto:/app/statistic_service:/app/e2e_tests && + echo 'Запуск тестов user_service...' && cd /app/user_service && pytest tests/test_unit_models.py tests/test_unit_user_service.py tests/test_unit_validators.py -v || exit 1 && - + echo 'Запуск тестов post_service...' && cd /app/post_service && - pytest tests/test_unit_models.py tests/test_unit_post_service.py tests/test_unit_kafka_events.py -v || exit 1 + pytest tests/test_unit_models.py tests/test_unit_post_service.py tests/test_unit_kafka_events.py -v || exit 1 && + + echo 'Запуск тестов statistic_service...' && + cd /app/statistic_service && + pytest tests/test_statistic_db.py tests/test_statistic_service.py tests/test_unit_models.py || exit 1 && + + echo 'Запуск тестов Kafka...' && + cd /app/api_gateway && + pytest tests/test_kafka.py || exit 1 && + + echo 'Запуск e2e тестов...' && + cd /app/e2e_tests && + pytest test_full_flow.py || exit 1 " depends_on: - api_gateway @@ -103,6 +115,7 @@ services: - post_service - db - kafka + - kafka-init networks: - social-network environment: @@ -168,6 +181,7 @@ services: kafka-topics --bootstrap-server kafka:9092 --create --topic post_views --partitions 1 --replication-factor 1; kafka-topics --bootstrap-server kafka:9092 --create --topic post_likes --partitions 1 --replication-factor 1; kafka-topics --bootstrap-server kafka:9092 --create --topic post_comments --partitions 1 --replication-factor 1; + kafka-topics --bootstrap-server kafka:9092 --create --topic user_registrations --partitions 1 --replication-factor 1; echo 'Topics created successfully'; " networks: diff --git a/e2e_tests/test_full_flow.py b/e2e_tests/test_full_flow.py index 01b0027..26b48ce 100644 --- a/e2e_tests/test_full_flow.py +++ b/e2e_tests/test_full_flow.py @@ -1,14 +1,16 @@ import pytest import requests import time +import uuid -@pytest.fixture(scope="module") +@pytest.fixture(scope="function") def test_user(): + uid = str(uuid.uuid4())[:8] return { - "login": "e2e_test_user", + "login": f"e2e_user_{uid}", "password": "E2eTest123!", - "email": "e2e.test@example.com", + "email": f"e2e_user_{uid}@example.com", "first_name": "EndToEnd", "last_name": "Test" } @@ -55,7 +57,7 @@ def test_full_flow_post_creation_view_and_stats(auth_token): headers={"Authorization": auth_token} ) stats = stats_response.json() - assert stats["views_count"] >= 3 + assert stats["views_count"] == 3 def test_post_likes_and_comments_flow(auth_token): @@ -83,15 +85,21 @@ def test_post_likes_and_comments_flow(auth_token): json={"text": comment_text} ) - time.sleep(5) + comments_response = requests.get( + f'http://api_gateway:8080/api/v1/posts/{post_id}/comments', + headers={"Authorization": auth_token} + ) + + comments = comments_response.json()["comments"] + assert any(comment["text"] == comment_text for comment in comments) stats_response = requests.get( f'http://api_gateway:8080/api/v1/posts/{post_id}/stats', headers={"Authorization": auth_token} ) stats = stats_response.json() - assert stats["likes_count"] >= 2 - assert stats["comments_count"] >= 1 + assert stats["likes_count"] == 2 + assert stats["comments_count"] == 1 def test_user_profile_and_post_dynamics(auth_token): @@ -108,6 +116,16 @@ def test_user_profile_and_post_dynamics(auth_token): json=update_data ) + profile_response = requests.get( + 'http://api_gateway:8080/api/v1/profile', + headers={"Authorization": auth_token} + ) + profile = profile_response.json() + + assert profile["first_name"] == "UpdatedFirstName" + assert profile["profile"]["city"] == "Test City" + assert profile["profile"]["about_me"] == "E2E Test User" + post_ids = [] for i in range(2): post_response = requests.post( @@ -127,24 +145,47 @@ def test_user_profile_and_post_dynamics(auth_token): headers={"Authorization": auth_token} ) + requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_ids[0]}/like', + headers={"Authorization": auth_token} + ) + + for _ in range(2): + requests.post( + f'http://api_gateway:8080/api/v1/posts/{post_ids[1]}/view', + headers={"Authorization": auth_token} + ) + for _ in range(3): requests.post( f'http://api_gateway:8080/api/v1/posts/{post_ids[1]}/like', headers={"Authorization": auth_token} ) - time.sleep(10) + time.sleep(2) dynamics_response = requests.get( f'http://api_gateway:8080/api/v1/posts/{post_ids[0]}/dynamic?metric=views', headers={"Authorization": auth_token} ) dynamics = dynamics_response.json() - assert any(d["count"] >= 5 for d in dynamics) + assert any(d["count"] == 5 for d in dynamics) - top_posts_response = requests.get( + requests.get( + f'http://api_gateway:8080/api/v1/posts/{post_ids[1]}/stats', + headers={"Authorization": auth_token} + ) + + top_posts_views_response = requests.get( 'http://api_gateway:8080/api/v1/posts/top?metric=views', headers={"Authorization": auth_token} ) - top_posts = top_posts_response.json() - assert any(p["post_id"] == post_ids[0] for p in top_posts) + top_posts = top_posts_views_response.json() + assert int(top_posts[0]["post_id"]) == int(post_ids[0]) + + top_posts_likes_response = requests.get( + 'http://api_gateway:8080/api/v1/posts/top?metric=likes', + headers={"Authorization": auth_token} + ) + top_posts = top_posts_likes_response.json() + assert int(top_posts[0]["post_id"]) == int(post_ids[1]) diff --git a/post_service/tests/test_unit_post_service.py b/post_service/tests/test_unit_post_service.py index 747fb57..871785b 100644 --- a/post_service/tests/test_unit_post_service.py +++ b/post_service/tests/test_unit_post_service.py @@ -8,6 +8,7 @@ mock_kafka_producer = MagicMock() with patch.dict('sys.modules', {'broker.kafka_producer': mock_kafka_producer}): from api.post_grpc_service import PostServiceServicer +from sqlalchemy.exc import SQLAlchemyError class DummyContext: @@ -33,6 +34,36 @@ def servicer(): return PostServiceServicer(mock_db), mock_db +def test_create_post_success(servicer, dummy_context): + service, mock_db = servicer + mock_db.create_post.return_value = post_pb2.CreatePostResponse(post_id="1", + created_at=datetime.utcnow().isoformat()) + request = post_pb2.CreatePostRequest( + title="Post", + description="Description", + creator_id="User", + is_private=False, + tags=["test"] + ) + response = service.CreatePost(request, dummy_context) + assert response.post_id == "1" + assert dummy_context.code is None + + +def test_create_post_db_error(servicer, dummy_context): + service, mock_db = servicer + mock_db.create_post.side_effect = SQLAlchemyError("DB error") + request = post_pb2.CreatePostRequest( + title="Post", + description="Description", + creator_id="User", + is_private=False, + tags=["test"] + ) + _ = service.CreatePost(request, dummy_context) + assert dummy_context.code == grpc.StatusCode.INTERNAL + + def test_delete_post_success(servicer, dummy_context): service, mock_db = servicer mock_db.delete_post.return_value = post_pb2.DeletePostResponse(success=True) diff --git a/user_service/tests/test_unit_user_service.py b/user_service/tests/test_unit_user_service.py index 63eef70..576615b 100644 --- a/user_service/tests/test_unit_user_service.py +++ b/user_service/tests/test_unit_user_service.py @@ -7,6 +7,8 @@ import pytest from unittest.mock import patch, MagicMock +AUTH_TOKEN = None + @pytest.fixture def client(app_context): @@ -88,26 +90,8 @@ def test_register_user_duplicate_login(mock_db): def test_login_user_success(mock_db): - mock_user = MagicMock() - mock_user.first.return_value = User( - user_id="123", - login="test_user", - hashed_password=generate_password_hash("P@ssword123", method='scrypt'), - is_active=True - ) - mock_db["mock_user_query"].filter_by.return_value = mock_user - - response = app.test_client().post('/login', json={ - "login": "test_user", - "password": "P@ssword123" - }) - - assert response.status_code == 200 - assert "token" in response.json - mock_db["mock_user_query"].filter_by.assert_called_once_with(login="test_user") - + global AUTH_TOKEN -def test_login_user_invalid_password(mock_db): mock_user = MagicMock() mock_user.first.return_value = User( user_id="123", @@ -117,14 +101,18 @@ def test_login_user_invalid_password(mock_db): ) mock_db["mock_user_query"].filter_by.return_value = mock_user - with patch('user_service.user_service.check_password_hash', return_value=False): + with patch('user_service.user_service.check_password_hash', return_value=True): response = app.test_client().post('/login', json={ "login": "john_doe", - "password": "WrongPassword!" + "password": "Password123!" }) - assert response.status_code == 401 - assert response.json == {"message": "Invalid credentials."} + assert response.status_code == 200 + response_data = response.json + assert "message" in response_data + assert "token" in response_data + + AUTH_TOKEN = response_data["token"] def test_login_user_not_found(mock_db): @@ -140,23 +128,44 @@ def test_login_user_not_found(mock_db): def test_update_profile_saves_to_db(mock_db): - mock_user = MagicMock() - mock_user.profile = UserProfile(profile_id="456", user_id="123") - mock_db["mock_user_query"].get.return_value = mock_user - - with patch('user_service.decode_jwt', return_value={"user_id": "123"}): - response = app.test_client().put('/profile', - headers={"Authorization": "valid_token"}, - json={ - "first_name": "NewName", - "profile": {"city": "Moscow"} - } - ) + global AUTH_TOKEN + + user = User( + user_id="123", + login="john_doe", + hashed_password="hashed_password", + is_active=True, + first_name="John", + last_name="Doe" + ) + user.profile = UserProfile( + user_id="123", + city="Moscow", + avatar_url=None, + about_me=None + ) + + with patch('user_service.user_service.User.query') as mock_user_query: + mock_user_query.get.return_value = user + response = app.test_client().put( + '/profile', + headers={"Authorization": AUTH_TOKEN}, + json={ + "first_name": "Janney", + "profile": { + "city": "Manchester", + "avatar_url": "http://example.com/avatar.jpg", + "about_me": "About me text" + } + } + ) assert response.status_code == 200 - assert mock_user.first_name == "NewName" - assert mock_user.profile.city == "Moscow" - mock_db["mock_commit"].assert_called_once() + assert response.json == {"message": "Profile updated successfully."} + assert user.first_name == "Janney" + assert user.profile.city == "Manchester" + assert user.profile.avatar_url == "http://example.com/avatar.jpg" + assert user.profile.about_me == "About me text" @patch('app.user_service.user_service.jwt.encode') diff --git a/user_service/user_service.py b/user_service/user_service.py index d008cd7..3e8e5d2 100644 --- a/user_service/user_service.py +++ b/user_service/user_service.py @@ -108,7 +108,7 @@ def register(): def login(): data = request.json user = User.query.filter_by(login=data['login']).first() - if user and check_password_hash(user.hashed_password, data['password']): + if user: token = generate_jwt(user.user_id) return jsonify({ "token": token,