fischer-agentkit/tests/unit/bitable/test_repository.py

114 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for BitableRepository — U3/DR-1 + DR-4 fixes.
DR-1: field_id UUID validation before SQL interpolation (pure unit test).
DR-4: atomic delete_view_if_not_last (requires PostgreSQL).
"""
from __future__ import annotations
import pytest
from agentkit.bitable.repository import _validate_field_id
# ── DR-1: field_id UUID validation ──────────────────────────
class TestValidateFieldId:
"""DR-1: field_id 校验 — 拒绝非 UUID 格式进入 SQL 结构插值。"""
def test_valid_uuid_passes(self):
valid = "550e8400-e29b-41d4-a716-446655440000"
assert _validate_field_id(valid) == valid
def test_valid_uppercase_uuid_passes(self):
valid = "550E8400-E29B-41D4-A716-446655440000"
assert _validate_field_id(valid) == valid
@pytest.mark.parametrize(
"bad_field_id,label",
[
("not-a-uuid", "arbitrary string"),
("", "empty string"),
("550e8400-e29b-41d4-a716", "too short"),
("550e8400-e29b-41d4-a716-446655440000-extra", "too long"),
("550e8400-e29b-41d4-a716-44665544000g", "non-hex char"),
("'; DROP TABLE bitable.bitable_views; --", "SQL injection attempt"),
("values->>'field' = :fv0", "SQL fragment"),
],
)
def test_invalid_field_id_raises(self, bad_field_id: str, label: str):
with pytest.raises(ValueError, match="Invalid field_id format"):
_validate_field_id(bad_field_id, context=label)
def test_non_string_raises(self):
with pytest.raises(ValueError, match="Invalid field_id format"):
_validate_field_id(None) # type: ignore[arg-type]
with pytest.raises(ValueError, match="Invalid field_id format"):
_validate_field_id(12345) # type: ignore[arg-type]
# ── DR-4: atomic delete_view_if_not_last ────────────────────
# These tests require PostgreSQL (SELECT FOR UPDATE behavior)
pytestmark_postgres = pytest.mark.postgres
@pytest.mark.postgres
class TestDeleteViewIfNotLastAtomic:
"""DR-4: 原子条件 delete_view — SELECT FOR UPDATE 事务防止 TOCTOU。
并发场景不变量:"table 至少 1 view" 在并发删除最后两个 view 时保持。
"""
async def test_delete_view_if_not_last_succeeds_when_multiple_siblings(
self, bitable_service, make_table
):
"""多 view 时删除成功。"""
from agentkit.bitable.repository import BitableRepository
table = await make_table(name="T1")
v1 = await bitable_service.create_view(table.id, name="v1")
await bitable_service.create_view(table.id, name="v2")
repo = BitableRepository(bitable_service._repo._db)
deleted = await repo.delete_view_if_not_last(v1.id, table.id)
assert deleted is True
# v1 gone, v2 still present
assert await bitable_service.get_view(v1.id) is None
assert (await bitable_service.list_views(table.id)) is not None
async def test_delete_view_if_not_last_refuses_last_view(self, bitable_service, make_table):
"""最后一个 view 时拒绝删除(返回 False不抛异常 — service 层负责抛 LastViewDeletionError"""
from agentkit.bitable.repository import BitableRepository
table = await make_table(name="T2")
only_view = await bitable_service.create_view(table.id, name="only")
repo = BitableRepository(bitable_service._repo._db)
deleted = await repo.delete_view_if_not_last(only_view.id, table.id)
assert deleted is False
# View still exists
assert await bitable_service.get_view(only_view.id) is not None
async def test_delete_view_if_not_last_wrong_table_returns_false(
self, bitable_service, make_table
):
"""view_id 不属于给定 table_id 时返回 False不误删其他 table 的 view"""
from agentkit.bitable.repository import BitableRepository
table_a = await make_table(name="TA")
table_b = await make_table(name="TB")
va = await bitable_service.create_view(table_a.id, name="va")
# table_b 有自己的 view
await bitable_service.create_view(table_b.id, name="vb1")
await bitable_service.create_view(table_b.id, name="vb2")
repo = BitableRepository(bitable_service._repo._db)
# 用 table_b 的 id 但 view_id 是 table_a 的 — 不应删除
deleted = await repo.delete_view_if_not_last(va.id, table_b.id)
assert deleted is False
# va 仍然存在
assert await bitable_service.get_view(va.id) is not None