Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
lint-and-test:
uses: esclient/tools/.github/workflows/lint-and-test-python.yml@v1.0.0
with:
python-version: '3.13.7'
python-version: "3.13.7"
source: "commentservice"
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
11 changes: 5 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM python:3.13-slim

RUN apt-get update && apt-get install -y wget curl jq && \
RUN apt-get update && apt-get install -y curl jq wget && \
wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && \
chmod +x /usr/local/bin/yq && \
apt-get remove -y wget && apt-get autoremove -y && \
Expand All @@ -9,15 +9,14 @@ RUN apt-get update && apt-get install -y wget curl jq && \
WORKDIR /app

COPY pyproject.toml pdm.lock ./
COPY . .
RUN pip install --no-cache-dir pdm && \
pdm export --without-hashes -f requirements > /tmp/req.txt && \
pip install --no-cache-dir -r /tmp/req.txt && \
pip uninstall -y pdm && \
rm /tmp/req.txt

COPY . .
RUN pip install --no-cache-dir -e .
RUN chmod +x tools/load_envs.sh
rm /tmp/req.txt && \
pip install --no-cache-dir -e . && \
chmod +x tools/load_envs.sh

ENV ENV=prod
ENV PYTHONPATH=src
Expand Down
3 changes: 2 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ LOAD_ENVS_URL := 'https://raw.githubusercontent.com/esclient/tools/refs/heads/ma
PROTO_TAG := 'v0.0.17'
PROTO_NAME := 'comment.proto'
TMP_DIR := '.proto'
OUT_DIR := 'src/commentservice/grpc'
SOURCE := 'commentservice'
OUT_DIR := 'src/' + SOURCE + '/grpc'

MKDIR_TOOLS := 'mkdir -p tools'

Expand Down
316 changes: 227 additions & 89 deletions pdm.lock

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ plugins = ["pydantic.mypy"]
module = "asyncpg.*"
ignore_missing_imports = true

[tool.pytest.ini_options]
addopts = "--import-mode=importlib"
asyncio_mode = "strict"
testpaths = ["tests"]
python_files = ["test_*.py"]

[tool.coverage.run]
source = ["src/commentservice"]
relative_files = true

[tool.coverage.report]
include = [
"src/commentservice/handler/*",
"src/commentservice/service/*",
"src/commentservice/repository/*",
]
omit = ["src/commentservice/server.py"]

[project]
name = "commentservice"
version = "0.0.1"
Expand Down Expand Up @@ -122,7 +140,11 @@ dev = [
"grpc-stubs==1.53.0.6",
"protobuf==6.32.1",
"pytest==8.4.2",
"pytest-asyncio==0.24.0",
"pytest-cov==5.0.0",
"pytest-mock==3.15.1",
"pytest-faker==2.0.0",
"Faker==37.11.0",
"black==25.9.0",
"isort==6.0.1",
"flake8==7.3.0",
Expand Down
27 changes: 19 additions & 8 deletions tests/handler/test_create_comment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from unittest.mock import AsyncMock

import grpc
import pytest
from faker import Faker
from pytest_mock import MockerFixture

from commentservice.grpc.comment_pb2 import (
Expand All @@ -9,20 +13,27 @@
from commentservice.service.service import CommentService


def test_create_comment_success(mocker: MockerFixture) -> None:
@pytest.mark.asyncio
async def test_create_comment_success(
mocker: MockerFixture, faker: Faker
) -> None:
ctx = mocker.Mock(spec=grpc.ServicerContext)
fake_service = mocker.Mock(spec=CommentService)
fake_service.create_comment.return_value = 42
new_id = faker.random_int(min=1, max=100000)
fake_service.create_comment = AsyncMock(return_value=new_id)

mod_id = faker.random_int(min=1, max=100000)
author_id = faker.random_int(min=1, max=100000)
text = faker.sentence()
request = CreateCommentRequest(
mod_id=7,
author_id=13,
text="Test text",
mod_id=mod_id, author_id=author_id, text=text
)

response = CreateComment(fake_service, request, ctx)
response = await CreateComment(fake_service, request, ctx)

assert isinstance(response, CreateCommentResponse)
assert response.comment_id == 42
assert response.comment_id == new_id

fake_service.create_comment.assert_called_once_with(7, 13, "Test text")
fake_service.create_comment.assert_awaited_once_with(
mod_id, author_id, text
)
47 changes: 47 additions & 0 deletions tests/handler/test_delete_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from unittest.mock import AsyncMock

import grpc
import pytest
from faker import Faker
from pytest_mock import MockerFixture

from commentservice.grpc.comment_pb2 import (
DeleteCommentRequest,
DeleteCommentResponse,
)
from commentservice.handler.delete_comment import DeleteComment
from commentservice.service.service import CommentService


@pytest.mark.asyncio
async def test_delete_comment_success(
mocker: MockerFixture, faker: Faker
) -> None:
ctx = mocker.Mock(spec=grpc.ServicerContext)
fake_service = mocker.Mock(spec=CommentService)
fake_service.delete_comment = AsyncMock(return_value=True)

comment_id = faker.random_int(min=1, max=100000)
request = DeleteCommentRequest(comment_id=comment_id)

response = await DeleteComment(fake_service, request, ctx)

assert isinstance(response, DeleteCommentResponse)
assert response.success is True
fake_service.delete_comment.assert_awaited_once_with(comment_id)


@pytest.mark.asyncio
async def test_delete_comment_not_found(
mocker: MockerFixture, faker: Faker
) -> None:
ctx = mocker.Mock(spec=grpc.ServicerContext)
fake_service = mocker.Mock(spec=CommentService)
fake_service.delete_comment = AsyncMock(return_value=False)

comment_id = faker.random_int(min=1, max=100000)
request = DeleteCommentRequest(comment_id=comment_id)
response = await DeleteComment(fake_service, request, ctx)

assert response.success is False
fake_service.delete_comment.assert_awaited_once_with(comment_id)
48 changes: 48 additions & 0 deletions tests/handler/test_edit_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from unittest.mock import AsyncMock

import grpc
import pytest
from faker import Faker
from pytest_mock import MockerFixture

from commentservice.grpc.comment_pb2 import (
EditCommentRequest,
EditCommentResponse,
)
from commentservice.handler.edit_comment import EditComment
from commentservice.service.service import CommentService


@pytest.mark.asyncio
async def test_edit_comment_success(
mocker: MockerFixture, faker: Faker
) -> None:
ctx = mocker.Mock(spec=grpc.ServicerContext)
fake_service = mocker.Mock(spec=CommentService)
fake_service.edit_comment = AsyncMock(return_value=True)

comment_id = faker.random_int(min=1, max=100000)
new_text = faker.sentence()
request = EditCommentRequest(comment_id=comment_id, text=new_text)
response = await EditComment(fake_service, request, ctx)

assert isinstance(response, EditCommentResponse)
assert response.success is True
fake_service.edit_comment.assert_awaited_once_with(comment_id, new_text)


@pytest.mark.asyncio
async def test_edit_comment_not_found(
mocker: MockerFixture, faker: Faker
) -> None:
ctx = mocker.Mock(spec=grpc.ServicerContext)
fake_service = mocker.Mock(spec=CommentService)
fake_service.edit_comment = AsyncMock(return_value=False)

comment_id = faker.random_int(min=1, max=100000)
new_text = faker.sentence()
request = EditCommentRequest(comment_id=comment_id, text=new_text)
response = await EditComment(fake_service, request, ctx)

assert response.success is False
fake_service.edit_comment.assert_awaited_once_with(comment_id, new_text)
66 changes: 66 additions & 0 deletions tests/handler/test_get_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from datetime import UTC, timedelta

import grpc
import pytest
from faker import Faker
from pytest_mock import MockerFixture

from commentservice.grpc.comment_pb2 import (
GetCommentsRequest,
GetCommentsResponse,
)
from commentservice.handler.get_comments import GetComments, ts_to_dt
from commentservice.repository.model import Comment
from commentservice.service.service import CommentService


@pytest.mark.asyncio
async def test_get_comments_success(
mocker: MockerFixture, faker: Faker
) -> None:
ctx = mocker.Mock(spec=grpc.ServicerContext)
fake_service = mocker.Mock(spec=CommentService)

now = faker.date_time(tzinfo=UTC)
earlier = now - timedelta(hours=faker.random_int(min=1, max=12))

comment1 = Comment(
id=faker.random_int(min=1, max=100000),
author_id=faker.random_int(min=1, max=100000),
text=faker.sentence(),
created_at=earlier,
edited_at=None,
)
comment2 = Comment(
id=faker.random_int(min=1, max=100000),
author_id=faker.random_int(min=1, max=100000),
text=faker.sentence(),
created_at=now,
edited_at=now,
)
comments = [comment1, comment2]
fake_service.get_comments.return_value = comments

mod_id = faker.random_int(min=1, max=100000)
request = GetCommentsRequest(mod_id=mod_id)
response = await GetComments(fake_service, request, ctx)

assert isinstance(response, GetCommentsResponse)
assert response.mod_id == mod_id
assert len(response.comments) == 2

c1 = response.comments[0]
assert c1.id == comment1.id
assert c1.author_id == comment1.author_id
assert c1.text == comment1.text
assert not c1.HasField("edited_at")
assert ts_to_dt(c1.created_at, tz=UTC) == earlier

c2 = response.comments[1]
assert c2.id == comment2.id
assert c2.author_id == comment2.author_id
assert c2.text == comment2.text
assert c2.HasField("edited_at")
assert ts_to_dt(c2.created_at, tz=UTC) == now

fake_service.get_comments.assert_called_once_with(mod_id=mod_id)
Loading