From e75631c4001aed9d3eec3edd905035f0dc4ec234 Mon Sep 17 00:00:00 2001 From: Tecquo <46904988+Tecquo@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:32:12 +0300 Subject: [PATCH 1/3] getRates --- src/apigateway/resolvers/query/rating.py | 40 ++++++++++++++++++++++ src/apigateway/resolvers/query/root.py | 6 ++++ src/apigateway/schema/query/rating.graphql | 13 +++++++ src/apigateway/schema/schema.graphql | 1 + src/apigateway/server.py | 4 +++ 5 files changed, 64 insertions(+) create mode 100644 src/apigateway/resolvers/query/rating.py create mode 100644 src/apigateway/schema/query/rating.graphql diff --git a/src/apigateway/resolvers/query/rating.py b/src/apigateway/resolvers/query/rating.py new file mode 100644 index 0000000..02cf7ef --- /dev/null +++ b/src/apigateway/resolvers/query/rating.py @@ -0,0 +1,40 @@ +from typing import Any + +from ariadne import ObjectType +from graphql import GraphQLResolveInfo +from pydantic import BaseModel, field_validator + +from apigateway.helpers.id_helper import validate_and_convert_id + +from ..grpc_error_wrapper import handle_grpc_errors + + +class GetRatesInput(BaseModel): + mod_id: int + + @field_validator("mod_id", mode="before") + def _mod_id(cls, v: Any) -> int: + return validate_and_convert_id(v, "mod_id") + + +rating_query = ObjectType("RatingQuery") + + +@rating_query.field("getRates") +@handle_grpc_errors +async def resolve_get_rates( + parent: object, info: GraphQLResolveInfo, input: GetRatesInput +) -> dict[str, int]: + data = GetRatesInput.model_validate(input) + client = info.context["clients"]["rating_service"] + resp = await client.get_rates(data.mod_id) + + # Calculating likes (rate_4 + rate_5) and dislikes (rate_1 + rate_2) + likes = resp.rate_4 + resp.rate_5 + dislikes = resp.rate_1 + resp.rate_2 + + return { + "rates_total": resp.rates_total, + "likes": likes, + "dislikes": dislikes, + } diff --git a/src/apigateway/resolvers/query/root.py b/src/apigateway/resolvers/query/root.py index b1413c1..fee8a72 100644 --- a/src/apigateway/resolvers/query/root.py +++ b/src/apigateway/resolvers/query/root.py @@ -18,3 +18,9 @@ def resolve_comment_root(obj: Any, info: GraphQLResolveInfo, **kwargs: Any) -> d @handle_grpc_errors def resolve_mod_root(obj: Any, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, Any]: return {} + + +@query.field("rating") +@handle_grpc_errors +def resolve_rating_root(obj: Any, info: GraphQLResolveInfo, **kwargs: Any) -> dict[str, Any]: + return {} diff --git a/src/apigateway/schema/query/rating.graphql b/src/apigateway/schema/query/rating.graphql new file mode 100644 index 0000000..4d5dddf --- /dev/null +++ b/src/apigateway/schema/query/rating.graphql @@ -0,0 +1,13 @@ +type RatingQuery { + getRates(input: GetRatesInput!): RatesInfo! +} + +type RatesInfo { + rates_total: Int! + likes: Int! + dislikes: Int! +} + +input GetRatesInput { + mod_id: ID! +} diff --git a/src/apigateway/schema/schema.graphql b/src/apigateway/schema/schema.graphql index b83af0e..662541b 100644 --- a/src/apigateway/schema/schema.graphql +++ b/src/apigateway/schema/schema.graphql @@ -7,4 +7,5 @@ type Mutation { type Query { comment: CommentQuery mod: ModQuery + rating: RatingQuery } diff --git a/src/apigateway/server.py b/src/apigateway/server.py index 914d1d0..00c4c19 100644 --- a/src/apigateway/server.py +++ b/src/apigateway/server.py @@ -9,9 +9,11 @@ from apigateway.clients.client_factory import GrpcClientFactory from apigateway.resolvers.mutation.comment import comment_mutation from apigateway.resolvers.mutation.mod import mod_mutation +from apigateway.resolvers.mutation.rating import rating_mutation from apigateway.resolvers.mutation.root import mutation from apigateway.resolvers.query.comment import comment_query from apigateway.resolvers.query.mod import mod_query +from apigateway.resolvers.query.rating import rating_query from apigateway.resolvers.query.root import query from apigateway.settings import Settings @@ -29,9 +31,11 @@ query, comment_query, mod_query, + rating_query, mutation, comment_mutation, mod_mutation, + rating_mutation, ) context_viewer = GQLContextViewer() From a8b60ee293e9f5df301897396b6932922bb12f7f Mon Sep 17 00:00:00 2001 From: Tecquo <46904988+Tecquo@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:42:47 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BB?= =?UTF-8?q?=D0=B0=D0=B9=D0=BA=D0=B8/=D0=B4=D0=B8=D0=B7=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=BA=D0=B8,=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B2=D0=BE=D0=B4=D0=B8=D0=BC=20=D0=BE=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=20=D0=BA=D0=BE=D0=BB-=D0=B2=D0=BE=20?= =?UTF-8?q?=D0=BE=D1=86=D0=B5=D0=BD=D0=BE=D0=BA=20=D0=BE=D1=82=201=20?= =?UTF-8?q?=D0=B4=D0=BE=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apigateway/resolvers/query/rating.py | 11 +++++------ src/apigateway/schema/query/rating.graphql | 7 +++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/apigateway/resolvers/query/rating.py b/src/apigateway/resolvers/query/rating.py index 02cf7ef..67b7181 100644 --- a/src/apigateway/resolvers/query/rating.py +++ b/src/apigateway/resolvers/query/rating.py @@ -29,12 +29,11 @@ async def resolve_get_rates( client = info.context["clients"]["rating_service"] resp = await client.get_rates(data.mod_id) - # Calculating likes (rate_4 + rate_5) and dislikes (rate_1 + rate_2) - likes = resp.rate_4 + resp.rate_5 - dislikes = resp.rate_1 + resp.rate_2 - return { "rates_total": resp.rates_total, - "likes": likes, - "dislikes": dislikes, + "rate_1": resp.rate_1, + "rate_2": resp.rate_2, + "rate_3": resp.rate_3, + "rate_4": resp.rate_4, + "rate_5": resp.rate_5, } diff --git a/src/apigateway/schema/query/rating.graphql b/src/apigateway/schema/query/rating.graphql index 4d5dddf..9c199d5 100644 --- a/src/apigateway/schema/query/rating.graphql +++ b/src/apigateway/schema/query/rating.graphql @@ -4,8 +4,11 @@ type RatingQuery { type RatesInfo { rates_total: Int! - likes: Int! - dislikes: Int! + rate_1: Int! + rate_2: Int! + rate_3: Int! + rate_4: Int! + rate_5: Int! } input GetRatesInput { From 3fabb5ad5db8b92bb7ff8e1e804a0cf0efcf2995 Mon Sep 17 00:00:00 2001 From: Andrey Kataev Date: Thu, 23 Oct 2025 19:34:55 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=81=D0=BE=D0=BD=D0=B0=D1=80=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/lint-and-test.yml | 4 +- justfile | 6 +- pyproject.toml | 10 +- src/apigateway/resolvers/query/rating.py | 6 +- src/apigateway/stubs/comment/comment_pb2.py | 43 +++--- src/apigateway/stubs/comment/comment_pb2.pyi | 9 +- src/apigateway/stubs/rating/rating_pb2.py | 12 +- src/apigateway/stubs/rating/rating_pb2.pyi | 18 ++- src/apigateway/stubs/user/user_pb2.py | 18 ++- src/apigateway/stubs/user/user_pb2.pyi | 14 ++ src/apigateway/stubs/user/user_pb2_grpc.py | 43 ++++++ tests/clients/test_base_client.py | 100 -------------- tests/clients/test_client_factory.py | 68 --------- tests/resolvers/test_comment_mutation.py | 84 ----------- tests/resolvers/test_grpc_error_wrapper.py | 51 ------- tests/resolvers/test_mod_mutation.py | 76 ---------- tests/resolvers/test_query_resolvers.py | 138 ------------------- tests/resolvers/test_rating_mutation.py | 47 ------- 18 files changed, 130 insertions(+), 617 deletions(-) delete mode 100644 tests/clients/test_base_client.py delete mode 100644 tests/clients/test_client_factory.py delete mode 100644 tests/resolvers/test_comment_mutation.py delete mode 100644 tests/resolvers/test_grpc_error_wrapper.py delete mode 100644 tests/resolvers/test_mod_mutation.py delete mode 100644 tests/resolvers/test_query_resolvers.py delete mode 100644 tests/resolvers/test_rating_mutation.py diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index d20dec1..a467c50 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -18,7 +18,7 @@ jobs: python-version: "3.13.7" source: "apigateway" sonar-inclusions: "src/**,Dockerfile" - sonar-exclusions: "**/stubs/**" - sonar-coverage-exclusions: "src/apigateway/server.py,src/apigateway/settings.py" + sonar-exclusions: "**/stubs/**,src/apigateway/clients/**,src/apigateway/resolvers/**" + sonar-coverage-exclusions: "src/apigateway/clients/**,src/apigateway/resolvers/**,src/apigateway/esclient_graphql.py,src/apigateway/server.py,src/apigateway/settings.py" secrets: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/justfile b/justfile index 9d86fdc..8dbb7e9 100644 --- a/justfile +++ b/justfile @@ -6,13 +6,13 @@ LOAD_ENVS_URL := 'https://raw.githubusercontent.com/esclient/tools/refs/heads/ma PROTO_REPO := 'https://raw.githubusercontent.com/esclient/protos' -COMMENT_PROTO_TAG := 'v0.0.8' +COMMENT_PROTO_TAG := 'v0.1.2' COMMENT_PROTO_NAME := 'comment.proto' -USER_PROTO_TAG := 'v0.0.8' +USER_PROTO_TAG := 'v0.1.2' USER_PROTO_NAME := 'user.proto' -RATING_PROTO_TAG := 'v0.0.15' +RATING_PROTO_TAG := 'v0.1.2' RATING_PROTO_NAME := 'rating.proto' MOD_PROTO_TAG := 'v0.1.2' diff --git a/pyproject.toml b/pyproject.toml index f724086..d59c443 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,15 @@ relative_files = true [tool.coverage.report] include = [ - "src/apigateway/*", + "src/apigateway/converters/*", + "src/apigateway/helpers/*", +] +omit = [ + "src/apigateway/clients/*", + "src/apigateway/resolvers/*", + "src/apigateway/esclient_graphql.py", + "src/apigateway/server.py", + "src/apigateway/settings.py", ] [project] diff --git a/src/apigateway/resolvers/query/rating.py b/src/apigateway/resolvers/query/rating.py index 67b7181..e71acad 100644 --- a/src/apigateway/resolvers/query/rating.py +++ b/src/apigateway/resolvers/query/rating.py @@ -22,13 +22,11 @@ def _mod_id(cls, v: Any) -> int: @rating_query.field("getRates") @handle_grpc_errors -async def resolve_get_rates( - parent: object, info: GraphQLResolveInfo, input: GetRatesInput -) -> dict[str, int]: +async def resolve_get_rates(parent: object, info: GraphQLResolveInfo, input: GetRatesInput) -> dict[str, int]: data = GetRatesInput.model_validate(input) client = info.context["clients"]["rating_service"] resp = await client.get_rates(data.mod_id) - + return { "rates_total": resp.rates_total, "rate_1": resp.rate_1, diff --git a/src/apigateway/stubs/comment/comment_pb2.py b/src/apigateway/stubs/comment/comment_pb2.py index 58ec73c..7a0be3d 100644 --- a/src/apigateway/stubs/comment/comment_pb2.py +++ b/src/apigateway/stubs/comment/comment_pb2.py @@ -22,33 +22,34 @@ _sym_db = _symbol_database.Default() +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rcomment.proto\x12\x07\x63omment\"]\n\x07\x43omment\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x11\n\tauthor_id\x18\x02 \x01(\x03\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x12\n\ncreated_at\x18\x04 \x01(\x03\x12\x11\n\tedited_at\x18\x05 \x01(\x03\"G\n\x14\x43reateCommentRequest\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\x12\x11\n\tauthor_id\x18\x02 \x01(\x03\x12\x0c\n\x04text\x18\x03 \x01(\t\"+\n\x15\x43reateCommentResponse\x12\x12\n\ncomment_id\x18\x01 \x01(\x03\"$\n\x12GetCommentsRequest\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\"I\n\x13GetCommentsResponse\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\x12\"\n\x08\x63omments\x18\x02 \x03(\x0b\x32\x10.comment.Comment\"*\n\x14\x44\x65leteCommentRequest\x12\x12\n\ncomment_id\x18\x01 \x01(\x03\"(\n\x15\x44\x65leteCommentResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"6\n\x12\x45\x64itCommentRequest\x12\x12\n\ncomment_id\x18\x01 \x01(\x03\x12\x0c\n\x04text\x18\x02 \x01(\t\"&\n\x13\x45\x64itCommentResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x32\xc4\x02\n\x0e\x43ommentService\x12N\n\rCreateComment\x12\x1d.comment.CreateCommentRequest\x1a\x1e.comment.CreateCommentResponse\x12H\n\x0bGetComments\x12\x1b.comment.GetCommentsRequest\x1a\x1c.comment.GetCommentsResponse\x12N\n\rDeleteComment\x12\x1d.comment.DeleteCommentRequest\x1a\x1e.comment.DeleteCommentResponse\x12H\n\x0b\x45\x64itComment\x12\x1b.comment.EditCommentRequest\x1a\x1c.comment.EditCommentResponseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rcomment.proto\x12\x07\x63omment\x1a\x1fgoogle/protobuf/timestamp.proto\"\x95\x01\n\x07\x43omment\x12\n\n\x02id\x18\x01 \x01(\x03\x12\x11\n\tauthor_id\x18\x02 \x01(\x03\x12\x0c\n\x04text\x18\x03 \x01(\t\x12.\n\ncreated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12-\n\tedited_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"G\n\x14\x43reateCommentRequest\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\x12\x11\n\tauthor_id\x18\x02 \x01(\x03\x12\x0c\n\x04text\x18\x03 \x01(\t\"+\n\x15\x43reateCommentResponse\x12\x12\n\ncomment_id\x18\x01 \x01(\x03\"$\n\x12GetCommentsRequest\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\"I\n\x13GetCommentsResponse\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\x12\"\n\x08\x63omments\x18\x02 \x03(\x0b\x32\x10.comment.Comment\"*\n\x14\x44\x65leteCommentRequest\x12\x12\n\ncomment_id\x18\x01 \x01(\x03\"(\n\x15\x44\x65leteCommentResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"6\n\x12\x45\x64itCommentRequest\x12\x12\n\ncomment_id\x18\x01 \x01(\x03\x12\x0c\n\x04text\x18\x02 \x01(\t\"&\n\x13\x45\x64itCommentResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x32\xc4\x02\n\x0e\x43ommentService\x12N\n\rCreateComment\x12\x1d.comment.CreateCommentRequest\x1a\x1e.comment.CreateCommentResponse\x12H\n\x0bGetComments\x12\x1b.comment.GetCommentsRequest\x1a\x1c.comment.GetCommentsResponse\x12N\n\rDeleteComment\x12\x1d.comment.DeleteCommentRequest\x1a\x1e.comment.DeleteCommentResponse\x12H\n\x0b\x45\x64itComment\x12\x1b.comment.EditCommentRequest\x1a\x1c.comment.EditCommentResponseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'comment_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_COMMENT']._serialized_start=26 - _globals['_COMMENT']._serialized_end=119 - _globals['_CREATECOMMENTREQUEST']._serialized_start=121 - _globals['_CREATECOMMENTREQUEST']._serialized_end=192 - _globals['_CREATECOMMENTRESPONSE']._serialized_start=194 - _globals['_CREATECOMMENTRESPONSE']._serialized_end=237 - _globals['_GETCOMMENTSREQUEST']._serialized_start=239 - _globals['_GETCOMMENTSREQUEST']._serialized_end=275 - _globals['_GETCOMMENTSRESPONSE']._serialized_start=277 - _globals['_GETCOMMENTSRESPONSE']._serialized_end=350 - _globals['_DELETECOMMENTREQUEST']._serialized_start=352 - _globals['_DELETECOMMENTREQUEST']._serialized_end=394 - _globals['_DELETECOMMENTRESPONSE']._serialized_start=396 - _globals['_DELETECOMMENTRESPONSE']._serialized_end=436 - _globals['_EDITCOMMENTREQUEST']._serialized_start=438 - _globals['_EDITCOMMENTREQUEST']._serialized_end=492 - _globals['_EDITCOMMENTRESPONSE']._serialized_start=494 - _globals['_EDITCOMMENTRESPONSE']._serialized_end=532 - _globals['_COMMENTSERVICE']._serialized_start=535 - _globals['_COMMENTSERVICE']._serialized_end=859 + _globals['_COMMENT']._serialized_start=60 + _globals['_COMMENT']._serialized_end=209 + _globals['_CREATECOMMENTREQUEST']._serialized_start=211 + _globals['_CREATECOMMENTREQUEST']._serialized_end=282 + _globals['_CREATECOMMENTRESPONSE']._serialized_start=284 + _globals['_CREATECOMMENTRESPONSE']._serialized_end=327 + _globals['_GETCOMMENTSREQUEST']._serialized_start=329 + _globals['_GETCOMMENTSREQUEST']._serialized_end=365 + _globals['_GETCOMMENTSRESPONSE']._serialized_start=367 + _globals['_GETCOMMENTSRESPONSE']._serialized_end=440 + _globals['_DELETECOMMENTREQUEST']._serialized_start=442 + _globals['_DELETECOMMENTREQUEST']._serialized_end=484 + _globals['_DELETECOMMENTRESPONSE']._serialized_start=486 + _globals['_DELETECOMMENTRESPONSE']._serialized_end=526 + _globals['_EDITCOMMENTREQUEST']._serialized_start=528 + _globals['_EDITCOMMENTREQUEST']._serialized_end=582 + _globals['_EDITCOMMENTRESPONSE']._serialized_start=584 + _globals['_EDITCOMMENTRESPONSE']._serialized_end=622 + _globals['_COMMENTSERVICE']._serialized_start=625 + _globals['_COMMENTSERVICE']._serialized_end=949 # @@protoc_insertion_point(module_scope) diff --git a/src/apigateway/stubs/comment/comment_pb2.pyi b/src/apigateway/stubs/comment/comment_pb2.pyi index 407d6ee..7b44b4f 100644 --- a/src/apigateway/stubs/comment/comment_pb2.pyi +++ b/src/apigateway/stubs/comment/comment_pb2.pyi @@ -1,3 +1,6 @@ +import datetime + +from google.protobuf import timestamp_pb2 as _timestamp_pb2 from google.protobuf.internal import containers as _containers from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message @@ -16,9 +19,9 @@ class Comment(_message.Message): id: int author_id: int text: str - created_at: int - edited_at: int - def __init__(self, id: _Optional[int] = ..., author_id: _Optional[int] = ..., text: _Optional[str] = ..., created_at: _Optional[int] = ..., edited_at: _Optional[int] = ...) -> None: ... + created_at: _timestamp_pb2.Timestamp + edited_at: _timestamp_pb2.Timestamp + def __init__(self, id: _Optional[int] = ..., author_id: _Optional[int] = ..., text: _Optional[str] = ..., created_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., edited_at: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... class CreateCommentRequest(_message.Message): __slots__ = ("mod_id", "author_id", "text") diff --git a/src/apigateway/stubs/rating/rating_pb2.py b/src/apigateway/stubs/rating/rating_pb2.py index 059749b..3223719 100644 --- a/src/apigateway/stubs/rating/rating_pb2.py +++ b/src/apigateway/stubs/rating/rating_pb2.py @@ -24,15 +24,15 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0crating.proto\x12\x06rating\"O\n\x0eRateModRequest\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\x12\x11\n\tauthor_id\x18\x02 \x01(\x03\x12\x1a\n\x04rate\x18\x03 \x01(\x0e\x32\x0c.rating.Rate\"\"\n\x0fRateModResponse\x12\x0f\n\x07rate_id\x18\x01 \x01(\x03\"!\n\x0fGetRatesRequest\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\"H\n\x10GetRatesResponse\x12\x13\n\x0brates_total\x18\x01 \x01(\x03\x12\r\n\x05likes\x18\x02 \x01(\x03\x12\x10\n\x08\x64islikes\x18\x03 \x01(\x03*X\n\x04Rate\x12\x14\n\x10RATE_UNSPECIFIED\x10\x00\x12\n\n\x06RATE_1\x10\x01\x12\n\n\x06RATE_2\x10\x02\x12\n\n\x06RATE_3\x10\x03\x12\n\n\x06RATE_4\x10\x04\x12\n\n\x06RATE_5\x10\x05\x32\x8a\x01\n\rRatingService\x12:\n\x07RateMod\x12\x16.rating.RateModRequest\x1a\x17.rating.RateModResponse\x12=\n\x08GetRates\x12\x17.rating.GetRatesRequest\x1a\x18.rating.GetRatesResponseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0crating.proto\x12\x06rating\"O\n\x0eRateModRequest\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\x12\x11\n\tauthor_id\x18\x02 \x01(\x03\x12\x1a\n\x04rate\x18\x03 \x01(\x0e\x32\x0c.rating.Rate\"\"\n\x0fRateModResponse\x12\x0f\n\x07rate_id\x18\x01 \x01(\x03\"!\n\x0fGetRatesRequest\x12\x0e\n\x06mod_id\x18\x01 \x01(\x03\"w\n\x10GetRatesResponse\x12\x13\n\x0brates_total\x18\x01 \x01(\x03\x12\x0e\n\x06rate_1\x18\x02 \x01(\x03\x12\x0e\n\x06rate_2\x18\x03 \x01(\x03\x12\x0e\n\x06rate_3\x18\x04 \x01(\x03\x12\x0e\n\x06rate_4\x18\x05 \x01(\x03\x12\x0e\n\x06rate_5\x18\x06 \x01(\x03*X\n\x04Rate\x12\x14\n\x10RATE_UNSPECIFIED\x10\x00\x12\n\n\x06RATE_1\x10\x01\x12\n\n\x06RATE_2\x10\x02\x12\n\n\x06RATE_3\x10\x03\x12\n\n\x06RATE_4\x10\x04\x12\n\n\x06RATE_5\x10\x05\x32\x8a\x01\n\rRatingService\x12:\n\x07RateMod\x12\x16.rating.RateModRequest\x1a\x17.rating.RateModResponse\x12=\n\x08GetRates\x12\x17.rating.GetRatesRequest\x1a\x18.rating.GetRatesResponseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'rating_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_RATE']._serialized_start=250 - _globals['_RATE']._serialized_end=338 + _globals['_RATE']._serialized_start=297 + _globals['_RATE']._serialized_end=385 _globals['_RATEMODREQUEST']._serialized_start=24 _globals['_RATEMODREQUEST']._serialized_end=103 _globals['_RATEMODRESPONSE']._serialized_start=105 @@ -40,7 +40,7 @@ _globals['_GETRATESREQUEST']._serialized_start=141 _globals['_GETRATESREQUEST']._serialized_end=174 _globals['_GETRATESRESPONSE']._serialized_start=176 - _globals['_GETRATESRESPONSE']._serialized_end=248 - _globals['_RATINGSERVICE']._serialized_start=341 - _globals['_RATINGSERVICE']._serialized_end=479 + _globals['_GETRATESRESPONSE']._serialized_end=295 + _globals['_RATINGSERVICE']._serialized_start=388 + _globals['_RATINGSERVICE']._serialized_end=526 # @@protoc_insertion_point(module_scope) diff --git a/src/apigateway/stubs/rating/rating_pb2.pyi b/src/apigateway/stubs/rating/rating_pb2.pyi index 5029be1..1557ae6 100644 --- a/src/apigateway/stubs/rating/rating_pb2.pyi +++ b/src/apigateway/stubs/rating/rating_pb2.pyi @@ -43,11 +43,17 @@ class GetRatesRequest(_message.Message): def __init__(self, mod_id: _Optional[int] = ...) -> None: ... class GetRatesResponse(_message.Message): - __slots__ = ("rates_total", "likes", "dislikes") + __slots__ = ("rates_total", "rate_1", "rate_2", "rate_3", "rate_4", "rate_5") RATES_TOTAL_FIELD_NUMBER: _ClassVar[int] - LIKES_FIELD_NUMBER: _ClassVar[int] - DISLIKES_FIELD_NUMBER: _ClassVar[int] + RATE_1_FIELD_NUMBER: _ClassVar[int] + RATE_2_FIELD_NUMBER: _ClassVar[int] + RATE_3_FIELD_NUMBER: _ClassVar[int] + RATE_4_FIELD_NUMBER: _ClassVar[int] + RATE_5_FIELD_NUMBER: _ClassVar[int] rates_total: int - likes: int - dislikes: int - def __init__(self, rates_total: _Optional[int] = ..., likes: _Optional[int] = ..., dislikes: _Optional[int] = ...) -> None: ... + rate_1: int + rate_2: int + rate_3: int + rate_4: int + rate_5: int + def __init__(self, rates_total: _Optional[int] = ..., rate_1: _Optional[int] = ..., rate_2: _Optional[int] = ..., rate_3: _Optional[int] = ..., rate_4: _Optional[int] = ..., rate_5: _Optional[int] = ...) -> None: ... diff --git a/src/apigateway/stubs/user/user_pb2.py b/src/apigateway/stubs/user/user_pb2.py index d2bff77..facaf11 100644 --- a/src/apigateway/stubs/user/user_pb2.py +++ b/src/apigateway/stubs/user/user_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nuser.proto\x12\x04user\"?\n\x10LoginUserRequest\x12\x19\n\x11username_or_email\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\"_\n\x13RegisterUserRequest\x12\r\n\x05login\x18\x01 \x01(\t\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x10\n\x08password\x18\x03 \x01(\t\x12\x18\n\x10\x63onfirm_password\x18\x04 \x01(\t\"$\n\x11LoginUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\x03\"\'\n\x14RegisterUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\x03\x32\x92\x01\n\x0bUserService\x12<\n\tLoginUser\x12\x16.user.LoginUserRequest\x1a\x17.user.LoginUserResponse\x12\x45\n\x0cRegisterUser\x12\x19.user.RegisterUserRequest\x1a\x1a.user.RegisterUserResponseB\"Z github.com/esclient/user-serviceb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nuser.proto\x12\x04user\"?\n\x10LoginUserRequest\x12\x19\n\x11username_or_email\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\"_\n\x13RegisterUserRequest\x12\r\n\x05login\x18\x01 \x01(\t\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x10\n\x08password\x18\x03 \x01(\t\x12\x18\n\x10\x63onfirm_password\x18\x04 \x01(\t\"?\n\x11VerifyUserRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\x03\x12\x19\n\x11\x63onfirmation_code\x18\x02 \x01(\t\"$\n\x11LoginUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\x03\"\'\n\x14RegisterUserResponse\x12\x0f\n\x07user_id\x18\x01 \x01(\x03\")\n\x12VerifyUserResponse\x12\x13\n\x0bis_verified\x18\x01 \x01(\x08\x32\xd3\x01\n\x0bUserService\x12<\n\tLoginUser\x12\x16.user.LoginUserRequest\x1a\x17.user.LoginUserResponse\x12\x45\n\x0cRegisterUser\x12\x19.user.RegisterUserRequest\x1a\x1a.user.RegisterUserResponse\x12?\n\nVerifyUser\x12\x17.user.VerifyUserRequest\x1a\x18.user.VerifyUserResponseB\"Z github.com/esclient/user-serviceb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -36,10 +36,14 @@ _globals['_LOGINUSERREQUEST']._serialized_end=83 _globals['_REGISTERUSERREQUEST']._serialized_start=85 _globals['_REGISTERUSERREQUEST']._serialized_end=180 - _globals['_LOGINUSERRESPONSE']._serialized_start=182 - _globals['_LOGINUSERRESPONSE']._serialized_end=218 - _globals['_REGISTERUSERRESPONSE']._serialized_start=220 - _globals['_REGISTERUSERRESPONSE']._serialized_end=259 - _globals['_USERSERVICE']._serialized_start=262 - _globals['_USERSERVICE']._serialized_end=408 + _globals['_VERIFYUSERREQUEST']._serialized_start=182 + _globals['_VERIFYUSERREQUEST']._serialized_end=245 + _globals['_LOGINUSERRESPONSE']._serialized_start=247 + _globals['_LOGINUSERRESPONSE']._serialized_end=283 + _globals['_REGISTERUSERRESPONSE']._serialized_start=285 + _globals['_REGISTERUSERRESPONSE']._serialized_end=324 + _globals['_VERIFYUSERRESPONSE']._serialized_start=326 + _globals['_VERIFYUSERRESPONSE']._serialized_end=367 + _globals['_USERSERVICE']._serialized_start=370 + _globals['_USERSERVICE']._serialized_end=581 # @@protoc_insertion_point(module_scope) diff --git a/src/apigateway/stubs/user/user_pb2.pyi b/src/apigateway/stubs/user/user_pb2.pyi index f744578..1baad9d 100644 --- a/src/apigateway/stubs/user/user_pb2.pyi +++ b/src/apigateway/stubs/user/user_pb2.pyi @@ -24,6 +24,14 @@ class RegisterUserRequest(_message.Message): confirm_password: str def __init__(self, login: _Optional[str] = ..., email: _Optional[str] = ..., password: _Optional[str] = ..., confirm_password: _Optional[str] = ...) -> None: ... +class VerifyUserRequest(_message.Message): + __slots__ = ("user_id", "confirmation_code") + USER_ID_FIELD_NUMBER: _ClassVar[int] + CONFIRMATION_CODE_FIELD_NUMBER: _ClassVar[int] + user_id: int + confirmation_code: str + def __init__(self, user_id: _Optional[int] = ..., confirmation_code: _Optional[str] = ...) -> None: ... + class LoginUserResponse(_message.Message): __slots__ = ("user_id",) USER_ID_FIELD_NUMBER: _ClassVar[int] @@ -35,3 +43,9 @@ class RegisterUserResponse(_message.Message): USER_ID_FIELD_NUMBER: _ClassVar[int] user_id: int def __init__(self, user_id: _Optional[int] = ...) -> None: ... + +class VerifyUserResponse(_message.Message): + __slots__ = ("is_verified",) + IS_VERIFIED_FIELD_NUMBER: _ClassVar[int] + is_verified: bool + def __init__(self, is_verified: bool = ...) -> None: ... diff --git a/src/apigateway/stubs/user/user_pb2_grpc.py b/src/apigateway/stubs/user/user_pb2_grpc.py index ee4f206..dbec8c2 100644 --- a/src/apigateway/stubs/user/user_pb2_grpc.py +++ b/src/apigateway/stubs/user/user_pb2_grpc.py @@ -44,6 +44,11 @@ def __init__(self, channel): request_serializer=user__pb2.RegisterUserRequest.SerializeToString, response_deserializer=user__pb2.RegisterUserResponse.FromString, _registered_method=True) + self.VerifyUser = channel.unary_unary( + '/user.UserService/VerifyUser', + request_serializer=user__pb2.VerifyUserRequest.SerializeToString, + response_deserializer=user__pb2.VerifyUserResponse.FromString, + _registered_method=True) class UserServiceServicer(object): @@ -61,6 +66,12 @@ def RegisterUser(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def VerifyUser(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_UserServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -74,6 +85,11 @@ def add_UserServiceServicer_to_server(servicer, server): request_deserializer=user__pb2.RegisterUserRequest.FromString, response_serializer=user__pb2.RegisterUserResponse.SerializeToString, ), + 'VerifyUser': grpc.unary_unary_rpc_method_handler( + servicer.VerifyUser, + request_deserializer=user__pb2.VerifyUserRequest.FromString, + response_serializer=user__pb2.VerifyUserResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'user.UserService', rpc_method_handlers) @@ -138,3 +154,30 @@ def RegisterUser(request, timeout, metadata, _registered_method=True) + + @staticmethod + def VerifyUser(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, + '/user.UserService/VerifyUser', + user__pb2.VerifyUserRequest.SerializeToString, + user__pb2.VerifyUserResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/tests/clients/test_base_client.py b/tests/clients/test_base_client.py deleted file mode 100644 index 1cdc63e..0000000 --- a/tests/clients/test_base_client.py +++ /dev/null @@ -1,100 +0,0 @@ -import asyncio -from types import SimpleNamespace -from typing import Any, cast - -import grpc -import grpc.aio -import pytest -from faker import Faker -from google.protobuf import message as _message - -from apigateway.clients.base_client import GrpcClient, GrpcError - - -class _FakeChannel: - def __init__(self) -> None: - self.closed = False - - async def close(self) -> None: - self.closed = True - - -class _FakeRpcError(grpc.RpcError): - def __init__(self, status_code: grpc.StatusCode) -> None: - super().__init__() - self._status_code = status_code - - def code(self) -> grpc.StatusCode: # type: ignore[override] - return self._status_code - - -class _ConcreteClient(GrpcClient[SimpleNamespace]): - def __init__(self, channel: _FakeChannel) -> None: - self.stub_initialized = False - super().__init__(cast(grpc.aio.Channel, channel)) - - def _initialize_stub(self) -> SimpleNamespace: - self.stub_initialized = True - return SimpleNamespace() - - -@pytest.mark.asyncio -async def test_call_invokes_rpc_method_with_timeout(faker: Faker) -> None: - channel = _FakeChannel() - client = _ConcreteClient(channel) - captured: dict[str, Any] = {} - request_payload = cast(_message.Message, SimpleNamespace(payload=faker.pystr())) - expected_response = faker.pystr() - timeout_value = faker.random_int(min=1, max=60) - - async def rpc_method(request: _message.Message, timeout: int = 30) -> str: - await asyncio.sleep(0) - captured["request"] = request - captured["timeout"] = timeout - return expected_response - - result = await client.call(rpc_method, request=request_payload, timeout=timeout_value) - - assert result == expected_response - assert captured == {"request": request_payload, "timeout": timeout_value} - - -@pytest.mark.asyncio -async def test_call_wraps_grpc_errors() -> None: - channel = _FakeChannel() - client = _ConcreteClient(channel) - - async def failing_rpc(request: _message.Message, timeout: int = 30) -> str: - await asyncio.sleep(0) - raise _FakeRpcError(grpc.StatusCode.INVALID_ARGUMENT) - - with pytest.raises(GrpcError) as exc: - await client.call(failing_rpc, request=cast(_message.Message, SimpleNamespace())) - - assert "gRPC" in str(exc.value) - - -@pytest.mark.asyncio -async def test_call_propagates_other_exceptions(faker: Faker) -> None: - channel = _FakeChannel() - client = _ConcreteClient(channel) - error_message = faker.sentence(nb_words=3) - - async def failing_rpc(request: _message.Message, timeout: int = 30) -> str: - await asyncio.sleep(0) - raise RuntimeError(error_message) - - with pytest.raises(RuntimeError) as exc: - await client.call(failing_rpc, request=cast(_message.Message, SimpleNamespace())) - - assert str(exc.value) == error_message - - -@pytest.mark.asyncio -async def test_close_closes_channel() -> None: - channel = _FakeChannel() - client = _ConcreteClient(channel) - - await client.close() - - assert channel.closed is True diff --git a/tests/clients/test_client_factory.py b/tests/clients/test_client_factory.py deleted file mode 100644 index 5f9ef49..0000000 --- a/tests/clients/test_client_factory.py +++ /dev/null @@ -1,68 +0,0 @@ -from collections.abc import Callable -from typing import Any -from unittest.mock import AsyncMock - -import pytest -from faker import Faker - -from apigateway.clients.client_factory import GrpcClientFactory - - -class _StubChannel: - def __init__(self) -> None: - self.closed = False - - def unary_unary(self, *_: Any, **__: Any) -> Callable[..., Any]: - async_mock = AsyncMock() - async_mock.__name__ = "unary_unary_mock" - return async_mock - - async def close(self) -> None: - self.closed = True - - -@pytest.fixture -def stub_channel_factory(monkeypatch: pytest.MonkeyPatch): - created_channels: list[_StubChannel] = [] - - def _factory(_: str) -> _StubChannel: - channel = _StubChannel() - created_channels.append(channel) - return channel - - monkeypatch.setattr( - "apigateway.clients.client_factory.grpc.aio.insecure_channel", - _factory, - ) - return created_channels - - -def test_factory_reuses_channel_instances(stub_channel_factory: list[_StubChannel], faker: Faker) -> None: - comment_endpoint = f"{faker.hostname()}:{faker.port_number()}" - mod_endpoint = f"{faker.hostname()}:{faker.port_number()}" - rating_endpoint = f"{faker.hostname()}:{faker.port_number()}" - factory = GrpcClientFactory(comment_endpoint, mod_endpoint, rating_endpoint) - - comment_client_first = factory.get_comment_client() - comment_client_second = factory.get_comment_client() - - assert comment_client_first is not comment_client_second - assert comment_client_first._channel is comment_client_second._channel # type: ignore[attr-defined] - assert len(stub_channel_factory) == 1 - - -@pytest.mark.asyncio -async def test_close_all_closes_each_channel(stub_channel_factory: list[_StubChannel], faker: Faker) -> None: - comment_endpoint = f"{faker.hostname()}:{faker.port_number()}" - mod_endpoint = f"{faker.hostname()}:{faker.port_number()}" - rating_endpoint = f"{faker.hostname()}:{faker.port_number()}" - factory = GrpcClientFactory(comment_endpoint, mod_endpoint, rating_endpoint) - - factory.get_comment_client() - factory.get_mod_client() - factory.get_rating_client() - - await factory.close_all() - - assert len(stub_channel_factory) == 3 - assert all(channel.closed for channel in stub_channel_factory) diff --git a/tests/resolvers/test_comment_mutation.py b/tests/resolvers/test_comment_mutation.py deleted file mode 100644 index 4014756..0000000 --- a/tests/resolvers/test_comment_mutation.py +++ /dev/null @@ -1,84 +0,0 @@ -from types import SimpleNamespace -from unittest.mock import AsyncMock - -import pytest -from faker import Faker -from graphql import GraphQLError - -from apigateway.clients.base_client import GrpcError -from apigateway.resolvers.mutation.comment import ( - resolve_create_comment, - resolve_delete_comment, - resolve_edit_comment, -) - - -def build_info(**clients: object) -> SimpleNamespace: - return SimpleNamespace(context={"clients": clients}) - - -@pytest.mark.asyncio -async def test_resolve_create_comment_returns_new_id(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - author_id = faker.random_int(min=1) - comment_id = faker.random_int(min=1) - comment_text = faker.sentence(nb_words=5) - client = SimpleNamespace(create_comment=AsyncMock(return_value=SimpleNamespace(comment_id=comment_id))) - - result = await resolve_create_comment( - parent=None, - info=build_info(comment_service=client), - input={"mod_id": str(mod_id), "author_id": str(author_id), "text": comment_text}, - ) - - assert result == str(comment_id) - client.create_comment.assert_awaited_once_with(mod_id, author_id, comment_text) # type: ignore[attr-defined] - - -@pytest.mark.asyncio -async def test_resolve_edit_comment_returns_success_flag(faker: Faker) -> None: - comment_id = faker.random_int(min=1) - new_text = faker.sentence(nb_words=4) - client = SimpleNamespace(edit_comment=AsyncMock(return_value=SimpleNamespace(success=True))) - - result = await resolve_edit_comment( - parent=None, - info=build_info(comment_service=client), - input={"comment_id": str(comment_id), "text": new_text}, - ) - - assert result is True - client.edit_comment.assert_awaited_once_with(comment_id, new_text) # type: ignore[attr-defined] - - -@pytest.mark.asyncio -async def test_resolve_delete_comment_returns_success_flag(faker: Faker) -> None: - comment_id = faker.random_int(min=1) - client = SimpleNamespace(delete_comment=AsyncMock(return_value=SimpleNamespace(success=True))) - - result = await resolve_delete_comment( - parent=None, - info=build_info(comment_service=client), - input={"comment_id": str(comment_id)}, - ) - - assert result is True - client.delete_comment.assert_awaited_once_with(comment_id) # type: ignore[attr-defined] - - -@pytest.mark.asyncio -async def test_comment_mutations_wrap_grpc_errors(faker: Faker) -> None: - comment_id = faker.random_int(min=1) - error_message = faker.sentence(nb_words=6) - client = SimpleNamespace( - delete_comment=AsyncMock(side_effect=GrpcError(error_message)), - ) - - with pytest.raises(GraphQLError) as exc: - await resolve_delete_comment( - parent=None, - info=build_info(comment_service=client), - input={"comment_id": str(comment_id)}, - ) - - assert "gRPC" in str(exc.value) diff --git a/tests/resolvers/test_grpc_error_wrapper.py b/tests/resolvers/test_grpc_error_wrapper.py deleted file mode 100644 index 7c409f6..0000000 --- a/tests/resolvers/test_grpc_error_wrapper.py +++ /dev/null @@ -1,51 +0,0 @@ -import asyncio - -import pytest -from faker import Faker -from graphql import GraphQLError - -from apigateway.clients.base_client import GrpcError -from apigateway.resolvers.grpc_error_wrapper import handle_grpc_errors - - -@pytest.mark.asyncio -async def test_async_wrapper_transforms_grpc_error(faker: Faker) -> None: - error_message = faker.sentence(nb_words=3) - - @handle_grpc_errors - async def failing() -> None: - await asyncio.sleep(0) - raise GrpcError(error_message) - - with pytest.raises(GraphQLError) as exc: - await failing() - - assert "gRPC" in str(exc.value) - - -@pytest.mark.asyncio -async def test_async_wrapper_transforms_unknown_error(faker: Faker) -> None: - error_message = faker.sentence(nb_words=3) - - @handle_grpc_errors - async def failing() -> None: - await asyncio.sleep(0) - raise RuntimeError(error_message) - - with pytest.raises(GraphQLError) as exc: - await failing() - - assert error_message in str(exc.value) - - -def test_sync_wrapper_transforms_grpc_error(faker: Faker) -> None: - error_message = faker.sentence(nb_words=3) - - @handle_grpc_errors - def failing() -> None: - raise GrpcError(error_message) - - with pytest.raises(GraphQLError) as exc: - failing() - - assert "gRPC" in str(exc.value) diff --git a/tests/resolvers/test_mod_mutation.py b/tests/resolvers/test_mod_mutation.py deleted file mode 100644 index 9c10d05..0000000 --- a/tests/resolvers/test_mod_mutation.py +++ /dev/null @@ -1,76 +0,0 @@ -from types import SimpleNamespace -from unittest.mock import AsyncMock - -import pytest -from faker import Faker -from graphql import GraphQLError - -from apigateway.clients.base_client import GrpcError -from apigateway.resolvers.mutation.mod import ( - ModStatus, - resolve_create_mod, - resolve_set_status_mod, -) - - -def build_info(**clients: object) -> SimpleNamespace: - return SimpleNamespace(context={"clients": clients}) - - -@pytest.mark.asyncio -async def test_resolve_create_mod_returns_payload_dict(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - s3_key = f"{mod_id}/{faker.file_name()}" - upload_url = faker.url() - title = faker.sentence(nb_words=3) - author_id = faker.random_int(min=1) - filename = faker.file_name(extension="zip") - description = faker.sentence(nb_words=6) - client = SimpleNamespace( - create_mod=AsyncMock(return_value=SimpleNamespace(mod_id=mod_id, s3_key=s3_key, upload_url=upload_url)) - ) - - result = await resolve_create_mod( - parent=None, - info=build_info(mod_service=client), - input={ - "title": title, - "author_id": str(author_id), - "filename": filename, - "description": description, - }, - ) - - assert result == {"mod_id": mod_id, "s3_key": s3_key, "upload_url": upload_url} - client.create_mod.assert_awaited_once_with(title, author_id, filename, description) # type: ignore[attr-defined] - - -@pytest.mark.asyncio -async def test_resolve_set_status_mod_returns_success(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - client = SimpleNamespace(set_status_mod=AsyncMock(return_value=SimpleNamespace(success=True))) - - result = await resolve_set_status_mod( - parent=None, - info=build_info(mod_service=client), - input={"mod_id": str(mod_id), "status": ModStatus.MOD_STATUS_BANNED}, - ) - - assert result is True - client.set_status_mod.assert_awaited_once_with(mod_id, ModStatus.MOD_STATUS_BANNED.value) # type: ignore[attr-defined] - - -@pytest.mark.asyncio -async def test_mod_mutations_wrap_grpc_errors(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - error_message = faker.sentence(nb_words=5) - client = SimpleNamespace(set_status_mod=AsyncMock(side_effect=GrpcError(error_message))) - - with pytest.raises(GraphQLError) as exc: - await resolve_set_status_mod( - parent=None, - info=build_info(mod_service=client), - input={"mod_id": str(mod_id), "status": ModStatus.MOD_STATUS_HIDDEN}, - ) - - assert "gRPC" in str(exc.value) diff --git a/tests/resolvers/test_query_resolvers.py b/tests/resolvers/test_query_resolvers.py deleted file mode 100644 index 84374ed..0000000 --- a/tests/resolvers/test_query_resolvers.py +++ /dev/null @@ -1,138 +0,0 @@ -from types import SimpleNamespace -from unittest.mock import AsyncMock - -import pytest -from faker import Faker -from graphql import GraphQLError - -from apigateway.clients.base_client import GrpcError -from apigateway.converters.mod_status_converter import PROTO_TO_GRAPHQL, proto_to_graphql_mod_status -from apigateway.resolvers.query.comment import resolve_get_comments -from apigateway.resolvers.query.mod import resolve_get_mod_download_link, resolve_get_mods - - -def build_info(**clients: object) -> SimpleNamespace: - return SimpleNamespace(context={"clients": clients}) - - -@pytest.mark.asyncio -async def test_resolve_get_mod_download_link_returns_url(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - link_url = faker.url() - mod_client = SimpleNamespace(get_mod_download_link=AsyncMock(return_value=SimpleNamespace(link_url=link_url))) - - result = await resolve_get_mod_download_link( - parent=None, - info=build_info(mod_service=mod_client), - input={"mod_id": str(mod_id)}, - ) - - assert result == link_url - mod_client.get_mod_download_link.assert_awaited_once_with(mod_id) # type: ignore[attr-defined] - - -@pytest.mark.asyncio -async def test_resolve_get_mods_maps_proto_fields(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - author_id = faker.random_int(min=1) - title = faker.sentence(nb_words=3) - description = faker.sentence() - version = faker.random_int(min=1, max=10) - status = faker.random_element(list(PROTO_TO_GRAPHQL.keys())) - created_at_seconds = faker.random_number(digits=6) - mod_client = SimpleNamespace( - get_mods=AsyncMock( - return_value=SimpleNamespace( - mods=[ - SimpleNamespace( - id=mod_id, - author_id=author_id, - title=title, - description=description, - version=version, - status=status, - created_at=SimpleNamespace(seconds=created_at_seconds), - ) - ] - ) - ) - ) - - result = await resolve_get_mods( - parent=None, - info=build_info(mod_service=mod_client), - ) - - assert result == [ - { - "id": mod_id, - "author_id": author_id, - "title": title, - "description": description, - "version": version, - "status": proto_to_graphql_mod_status(status), - "created_at": created_at_seconds, - } - ] - - -@pytest.mark.asyncio -async def test_resolve_get_comments_returns_serialized_comments(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - first_comment = SimpleNamespace( - id=faker.random_int(min=1), - text=faker.sentence(), - author_id=faker.random_int(min=1), - created_at=faker.random_int(min=1_000, max=9_999_999), - edited_at=0, - ) - second_comment = SimpleNamespace( - id=faker.random_int(min=first_comment.id + 1, max=first_comment.id + 10_000), - text=faker.sentence(), - author_id=faker.random_int(min=1), - created_at=faker.random_int(min=1_000, max=9_999_999), - edited_at=faker.random_int(min=1, max=9_999_999), - ) - comment_client = SimpleNamespace( - get_comments=AsyncMock(return_value=SimpleNamespace(comments=[first_comment, second_comment])) - ) - - result = await resolve_get_comments( - parent=None, - info=build_info(comment_service=comment_client), - input={"mod_id": str(mod_id)}, - ) - - expected = [ - { - "id": first_comment.id, - "text": first_comment.text, - "author_id": first_comment.author_id, - "created_at": first_comment.created_at, - "edited_at": None, - }, - { - "id": second_comment.id, - "text": second_comment.text, - "author_id": second_comment.author_id, - "created_at": second_comment.created_at, - "edited_at": second_comment.edited_at, - }, - ] - - assert result == expected - comment_client.get_comments.assert_awaited_once_with(mod_id) # type: ignore[attr-defined] - - -@pytest.mark.asyncio -async def test_query_resolvers_wrap_grpc_errors(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - error_message = faker.sentence(nb_words=5) - mod_client = SimpleNamespace(get_mod_download_link=AsyncMock(side_effect=GrpcError(error_message))) - - with pytest.raises(GraphQLError): - await resolve_get_mod_download_link( - parent=None, - info=build_info(mod_service=mod_client), - input={"mod_id": str(mod_id)}, - ) diff --git a/tests/resolvers/test_rating_mutation.py b/tests/resolvers/test_rating_mutation.py deleted file mode 100644 index 09ac5cf..0000000 --- a/tests/resolvers/test_rating_mutation.py +++ /dev/null @@ -1,47 +0,0 @@ -from types import SimpleNamespace -from unittest.mock import AsyncMock - -import pytest -from faker import Faker -from graphql import GraphQLError - -from apigateway.clients.base_client import GrpcError -from apigateway.resolvers.mutation.rating import RateType, resolve_add_rate - - -def build_info(**clients: object) -> SimpleNamespace: - return SimpleNamespace(context={"clients": clients}) - - -@pytest.mark.asyncio -async def test_resolve_add_rate_returns_rate_id(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - author_id = faker.random_int(min=1) - rate_id = faker.random_int(min=1) - client = SimpleNamespace(rate_mod=AsyncMock(return_value=SimpleNamespace(rate_id=rate_id))) - - result = await resolve_add_rate( - parent=None, - info=build_info(rating_service=client), - input={"mod_id": str(mod_id), "author_id": str(author_id), "rate": RateType.RATE_5}, - ) - - assert result == str(rate_id) - client.rate_mod.assert_awaited_once_with(mod_id, author_id, RateType.RATE_5.value) # type: ignore[attr-defined] - - -@pytest.mark.asyncio -async def test_rating_mutation_wraps_grpc_errors(faker: Faker) -> None: - mod_id = faker.random_int(min=1) - author_id = faker.random_int(min=1) - error_message = faker.sentence(nb_words=4) - client = SimpleNamespace(rate_mod=AsyncMock(side_effect=GrpcError(error_message))) - - with pytest.raises(GraphQLError) as exc: - await resolve_add_rate( - parent=None, - info=build_info(rating_service=client), - input={"mod_id": str(mod_id), "author_id": str(author_id), "rate": RateType.RATE_1}, - ) - - assert "gRPC" in str(exc.value)