test: migrate tag service tests to testcontainers (#38313)

This commit is contained in:
Escape0707 2026-07-02 20:58:56 +09:00 committed by GitHub
parent 12c06a61be
commit dcc06dee20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1029 additions and 1491 deletions

View File

@ -14,6 +14,9 @@ from models.enums import TagType
from models.model import App, Tag, TagBinding
from models.snippet import CustomizedSnippet
type _SessionLike = Session | scoped_session
type _TagTypeLike = TagType | str
class SaveTagPayload(BaseModel):
name: str = Field(min_length=1, max_length=50)
@ -38,7 +41,7 @@ class TagBindingDeletePayload(BaseModel):
class TagService:
@staticmethod
def get_tags(session: Session, tag_type: str, current_tenant_id: str, keyword: str | None = None):
def get_tags(session: Session, tag_type: _TagTypeLike, current_tenant_id: str, keyword: str | None = None):
stmt = (
select(Tag.id, Tag.type, Tag.name, func.count(TagBinding.id).label("binding_count"))
.outerjoin(TagBinding, Tag.id == TagBinding.tag_id)
@ -55,10 +58,10 @@ class TagService:
@staticmethod
def get_target_ids_by_tag_ids(
tag_type: str,
tag_type: _TagTypeLike,
current_tenant_id: str,
tag_ids: list[str],
session: scoped_session | Session,
session: _SessionLike,
*,
match_all: bool = False,
):
@ -104,7 +107,7 @@ class TagService:
return tag_bindings
@staticmethod
def get_tag_by_tag_name(tag_type: str, current_tenant_id: str, tag_name: str, session: scoped_session):
def get_tag_by_tag_name(tag_type: _TagTypeLike, current_tenant_id: str, tag_name: str, session: _SessionLike):
if not tag_type or not tag_name:
return []
tags = list(
@ -117,7 +120,7 @@ class TagService:
return tags
@staticmethod
def get_tags_by_target_id(tag_type: str, current_tenant_id: str, target_id: str, session: scoped_session):
def get_tags_by_target_id(tag_type: _TagTypeLike, current_tenant_id: str, target_id: str, session: _SessionLike):
tags = session.scalars(
select(Tag)
.join(TagBinding, Tag.id == TagBinding.tag_id)
@ -132,7 +135,7 @@ class TagService:
return tags or []
@staticmethod
def save_tags(payload: SaveTagPayload, session: scoped_session) -> Tag:
def save_tags(payload: SaveTagPayload, session: _SessionLike) -> Tag:
if TagService.get_tag_by_tag_name(payload.type, current_user.current_tenant_id, payload.name, session):
raise ValueError("Tag name already exists")
tag = Tag(
@ -148,7 +151,7 @@ class TagService:
@staticmethod
def update_tags(
payload: UpdateTagPayload, tag_id: str, session: scoped_session, *, tag_type: TagType | None = None
payload: UpdateTagPayload, tag_id: str, session: _SessionLike, *, tag_type: TagType | None = None
) -> Tag:
current_tenant_id = current_user.current_tenant_id
stmt = select(Tag).where(Tag.id == tag_id, Tag.tenant_id == current_tenant_id)
@ -175,7 +178,7 @@ class TagService:
return tag
@staticmethod
def get_tag_binding_count(tag_id: str, session: scoped_session, *, tag_type: TagType | None = None) -> int:
def get_tag_binding_count(tag_id: str, session: _SessionLike, *, tag_type: TagType | None = None) -> int:
current_tenant_id = current_user.current_tenant_id
stmt = (
select(func.count(TagBinding.id))
@ -188,7 +191,7 @@ class TagService:
return count
@staticmethod
def delete_tag(tag_id: str, session: scoped_session, *, tag_type: TagType | None = None):
def delete_tag(tag_id: str, session: _SessionLike, *, tag_type: TagType | None = None):
current_tenant_id = current_user.current_tenant_id
stmt = select(Tag).where(Tag.id == tag_id, Tag.tenant_id == current_tenant_id)
if tag_type is not None:
@ -207,7 +210,7 @@ class TagService:
session.commit()
@staticmethod
def save_tag_binding(payload: TagBindingCreatePayload, session: scoped_session):
def save_tag_binding(payload: TagBindingCreatePayload, session: _SessionLike):
TagService.check_target_exists(payload.type, payload.target_id, session)
valid_tag_ids = session.scalars(
select(Tag.id).where(
@ -234,7 +237,7 @@ class TagService:
session.commit()
@staticmethod
def delete_tag_binding(payload: TagBindingDeletePayload, session: scoped_session):
def delete_tag_binding(payload: TagBindingDeletePayload, session: _SessionLike):
TagService.check_target_exists(payload.type, payload.target_id, session)
result = cast(
CursorResult,
@ -257,7 +260,7 @@ class TagService:
session.commit()
@staticmethod
def check_target_exists(type: str, target_id: str, session: scoped_session):
def check_target_exists(type: _TagTypeLike, target_id: str, session: _SessionLike):
if type == "knowledge":
dataset = session.scalar(
select(Dataset)

View File

@ -115,7 +115,6 @@ project-excludes = [
"services/test_restore_archived_workflow_run.py",
"services/test_saved_message_service.py",
"services/test_schedule_service.py",
"services/test_tag_service.py",
"services/test_web_conversation_service.py",
"services/test_webapp_auth_service.py",
"services/test_webhook_service.py",

View File

@ -1,172 +0,0 @@
from types import SimpleNamespace
import pytest
from pytest_mock import MockerFixture
from werkzeug.exceptions import NotFound
from models.enums import TagType
from services.tag_service import TagBindingCreatePayload, TagBindingDeletePayload, TagService, UpdateTagPayload
@pytest.fixture
def current_user(mocker: MockerFixture):
user = SimpleNamespace(id="user-1", current_tenant_id="tenant-1")
mocker.patch("services.tag_service.current_user", user)
return user
@pytest.fixture
def db_session(mocker: MockerFixture):
mock_db = mocker.Mock()
return mock_db.session
def test_save_tag_binding_only_creates_bindings_for_valid_snippet_tags(mocker: MockerFixture, current_user, db_session):
mocker.patch("services.tag_service.TagService.check_target_exists")
db_session.scalars.return_value.all.return_value = ["tag-1"]
db_session.scalar.return_value = None
TagService.save_tag_binding(
TagBindingCreatePayload(
tag_ids=["tag-1", "tag-from-other-tenant"],
target_id="snippet-1",
type=TagType.SNIPPET,
),
db_session,
)
db_session.add.assert_called_once()
tag_binding = db_session.add.call_args.args[0]
assert tag_binding.tag_id == "tag-1"
assert tag_binding.target_id == "snippet-1"
assert tag_binding.tenant_id == current_user.current_tenant_id
assert tag_binding.created_by == current_user.id
db_session.commit.assert_called_once()
def test_delete_tag_binding_limits_deletion_to_valid_snippet_tags(mocker: MockerFixture, current_user, db_session):
mocker.patch("services.tag_service.TagService.check_target_exists")
db_session.execute.return_value = SimpleNamespace(rowcount=1)
TagService.delete_tag_binding(
TagBindingDeletePayload(
tag_ids=["tag-1", "tag-from-other-tenant"],
target_id="snippet-1",
type=TagType.SNIPPET,
),
db_session,
)
db_session.execute.assert_called_once()
db_session.commit.assert_called_once()
def test_delete_tag_binding_does_not_commit_when_no_rows_deleted(mocker: MockerFixture, current_user, db_session):
mocker.patch("services.tag_service.TagService.check_target_exists")
db_session.execute.return_value = SimpleNamespace(rowcount=0)
TagService.delete_tag_binding(
TagBindingDeletePayload(
tag_ids=["tag-1"],
target_id="snippet-1",
type=TagType.SNIPPET,
),
db_session,
)
db_session.execute.assert_called_once()
db_session.commit.assert_not_called()
def test_update_tags_scopes_lookup_to_current_tenant_and_type(current_user, db_session):
tag = SimpleNamespace(id="tag-1", name="old", type=TagType.KNOWLEDGE)
db_session.scalar.side_effect = [tag, None]
result = TagService.update_tags(UpdateTagPayload(name="new"), "tag-1", db_session, tag_type=TagType.KNOWLEDGE)
stmt = db_session.scalar.call_args_list[0].args[0]
compiled = stmt.compile()
statement = str(compiled)
assert "tags.id" in statement
assert "tags.tenant_id" in statement
assert "tags.type" in statement
assert "tag-1" in compiled.params.values()
assert current_user.current_tenant_id in compiled.params.values()
assert TagType.KNOWLEDGE in compiled.params.values()
assert result is tag
assert tag.name == "new"
db_session.commit.assert_called_once()
def test_get_tag_binding_count_scopes_lookup_to_current_tenant_and_type(current_user, db_session):
db_session.scalar.return_value = 3
result = TagService.get_tag_binding_count("tag-1", db_session, tag_type=TagType.KNOWLEDGE)
stmt = db_session.scalar.call_args.args[0]
compiled = stmt.compile()
statement = str(compiled)
assert "tag_bindings.tag_id" in statement
assert "tags.tenant_id" in statement
assert "tags.type" in statement
assert "tag-1" in compiled.params.values()
assert current_user.current_tenant_id in compiled.params.values()
assert TagType.KNOWLEDGE in compiled.params.values()
assert result == 3
def test_delete_tag_scopes_lookup_and_bindings_to_current_tenant(current_user, db_session):
tag = SimpleNamespace(id="tag-1", name="old", type=TagType.KNOWLEDGE)
binding = SimpleNamespace(id="binding-1")
db_session.scalar.return_value = tag
db_session.scalars.return_value.all.return_value = [binding]
TagService.delete_tag("tag-1", db_session, tag_type=TagType.KNOWLEDGE)
tag_stmt = db_session.scalar.call_args.args[0]
tag_compiled = tag_stmt.compile()
assert "tags.id" in str(tag_compiled)
assert "tags.tenant_id" in str(tag_compiled)
assert "tags.type" in str(tag_compiled)
assert "tag-1" in tag_compiled.params.values()
assert current_user.current_tenant_id in tag_compiled.params.values()
assert TagType.KNOWLEDGE in tag_compiled.params.values()
binding_stmt = db_session.scalars.call_args.args[0]
binding_compiled = binding_stmt.compile()
assert "tag_bindings.tag_id" in str(binding_compiled)
assert "tag_bindings.tenant_id" in str(binding_compiled)
assert "tag-1" in binding_compiled.params.values()
assert current_user.current_tenant_id in binding_compiled.params.values()
db_session.delete.assert_any_call(tag)
db_session.delete.assert_any_call(binding)
db_session.commit.assert_called_once()
def test_get_target_ids_by_tag_ids_returns_empty_without_query_for_empty_input(db_session):
result = TagService.get_target_ids_by_tag_ids(TagType.SNIPPET, "tenant-1", [], db_session)
assert result == []
db_session.scalars.assert_not_called()
def test_check_target_exists_accepts_existing_snippet(current_user, db_session):
db_session.scalar.return_value = SimpleNamespace(id="snippet-1")
TagService.check_target_exists("snippet", "snippet-1", db_session)
db_session.scalar.assert_called_once()
def test_check_target_exists_raises_when_snippet_missing(current_user, db_session):
db_session.scalar.return_value = None
with pytest.raises(NotFound, match="Snippet not found"):
TagService.check_target_exists("snippet", "missing-snippet", db_session)
def test_check_target_exists_raises_for_invalid_binding_type(current_user, db_session):
with pytest.raises(NotFound, match="Invalid binding type"):
TagService.check_target_exists("unknown", "target-1", db_session)
db_session.scalar.assert_not_called()