feat: REQ-005 assets table tag apply/remove SQL alignment (BAT-013)
- REQ-005 CHG-20260521-02 follow-up: clarify AC-1/AC-5 to target assets table - Add append_asset_tag_id/remove_asset_tag_id on PostgresPortfolioRepository - Replace internal calls in apply_asset_tag/remove_asset_tag routes (HTTP contract unchanged) - 7 new regression tests: method existence, SQL target table, error branches, idempotency guard - Update existing AC-1/AC-5 assertions to verify new methods + portfolios methods uncalled - Documentation: runtime-flows.md flow body + change log; rel-001 includes BAT-013 - REQ-005 transition: Approved → Implemented (CHG-20260521-03) via BAT-013 verification - Total tests: 362 → 369 passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1042,3 +1042,131 @@ def test_get_v1_assets_service_token_returns_403() -> None:
|
||||
for p in patches:
|
||||
p.stop()
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BAT-013: REQ-005 CHG-20260521-02 — assets 테이블 tag_ids mutation 정합화
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_postgres_repository_has_append_asset_tag_id_method() -> None:
|
||||
"""BAT-013 / REQ-005 AC-1: PostgresRepository에 append_asset_tag_id 메서드 존재."""
|
||||
from app.repositories.postgres import PostgresPortfolioRepository
|
||||
|
||||
assert hasattr(PostgresPortfolioRepository, "append_asset_tag_id")
|
||||
assert callable(PostgresPortfolioRepository.append_asset_tag_id)
|
||||
|
||||
|
||||
def test_postgres_repository_has_remove_asset_tag_id_method() -> None:
|
||||
"""BAT-013 / REQ-005 AC-5: PostgresRepository에 remove_asset_tag_id 메서드 존재."""
|
||||
from app.repositories.postgres import PostgresPortfolioRepository
|
||||
|
||||
assert hasattr(PostgresPortfolioRepository, "remove_asset_tag_id")
|
||||
assert callable(PostgresPortfolioRepository.remove_asset_tag_id)
|
||||
|
||||
|
||||
def test_append_asset_tag_id_targets_assets_table_not_portfolios() -> None:
|
||||
"""BAT-013 / REQ-005 AC-1: append_asset_tag_id SQL이 assets 테이블을 UPDATE한다 (portfolios 아님)."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from app.repositories.postgres import PostgresPortfolioRepository
|
||||
|
||||
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
||||
repo.ensure_schema = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.rowcount = 1
|
||||
mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
|
||||
mock_cursor.__exit__ = MagicMock(return_value=None)
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.cursor.return_value = mock_cursor
|
||||
|
||||
with patch.object(PostgresPortfolioRepository, "_connect", return_value=mock_conn):
|
||||
repo.append_asset_tag_id("asset-uuid-1", "tag-uuid-1")
|
||||
|
||||
sql_executed = mock_cursor.execute.call_args_list[0][0][0]
|
||||
assert "UPDATE assets" in sql_executed
|
||||
assert "UPDATE portfolios" not in sql_executed
|
||||
assert "WHERE asset_id" in sql_executed
|
||||
|
||||
|
||||
def test_remove_asset_tag_id_targets_assets_table_not_portfolios() -> None:
|
||||
"""BAT-013 / REQ-005 AC-5: remove_asset_tag_id SQL이 assets 테이블을 UPDATE한다 (portfolios 아님)."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from app.repositories.postgres import PostgresPortfolioRepository
|
||||
|
||||
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
||||
repo.ensure_schema = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.rowcount = 1
|
||||
mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
|
||||
mock_cursor.__exit__ = MagicMock(return_value=None)
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.cursor.return_value = mock_cursor
|
||||
|
||||
with patch.object(PostgresPortfolioRepository, "_connect", return_value=mock_conn):
|
||||
repo.remove_asset_tag_id("asset-uuid-1", "tag-uuid-1")
|
||||
|
||||
sql_executed = mock_cursor.execute.call_args_list[0][0][0]
|
||||
assert "UPDATE assets" in sql_executed
|
||||
assert "UPDATE portfolios" not in sql_executed
|
||||
assert "WHERE asset_id" in sql_executed
|
||||
|
||||
|
||||
def test_append_asset_tag_id_raises_when_asset_not_found() -> None:
|
||||
"""BAT-013 / REQ-005 EC-1: 존재하지 않는 asset_id는 ValueError."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from app.repositories.postgres import PostgresPortfolioRepository
|
||||
|
||||
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
||||
repo.ensure_schema = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.rowcount = 0
|
||||
mock_cursor.fetchone.return_value = None
|
||||
mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
|
||||
mock_cursor.__exit__ = MagicMock(return_value=None)
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.cursor.return_value = mock_cursor
|
||||
|
||||
with patch.object(PostgresPortfolioRepository, "_connect", return_value=mock_conn):
|
||||
with pytest.raises(ValueError, match="asset을 찾을 수 없습니다"):
|
||||
repo.append_asset_tag_id("missing-asset-uuid", "tag-uuid-1")
|
||||
|
||||
|
||||
def test_remove_asset_tag_id_raises_when_asset_not_found() -> None:
|
||||
"""BAT-013 / REQ-005 EC-1: DELETE 시 asset 부재면 ValueError."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from app.repositories.postgres import PostgresPortfolioRepository
|
||||
|
||||
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
||||
repo.ensure_schema = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.rowcount = 0
|
||||
mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
|
||||
mock_cursor.__exit__ = MagicMock(return_value=None)
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.cursor.return_value = mock_cursor
|
||||
|
||||
with patch.object(PostgresPortfolioRepository, "_connect", return_value=mock_conn):
|
||||
with pytest.raises(ValueError, match="asset을 찾을 수 없습니다"):
|
||||
repo.remove_asset_tag_id("missing-asset-uuid", "tag-uuid-1")
|
||||
|
||||
|
||||
def test_append_asset_tag_id_idempotent_guard_present() -> None:
|
||||
"""BAT-013 / REQ-005 EC-4: append_asset_tag_id SQL이 중복 방지 가드(`NOT ... = ANY(tag_ids)`)를 포함한다."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from app.repositories.postgres import PostgresPortfolioRepository
|
||||
|
||||
repo = PostgresPortfolioRepository.__new__(PostgresPortfolioRepository)
|
||||
repo.ensure_schema = MagicMock()
|
||||
mock_cursor = MagicMock()
|
||||
mock_cursor.rowcount = 1
|
||||
mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
|
||||
mock_cursor.__exit__ = MagicMock(return_value=None)
|
||||
mock_conn = MagicMock()
|
||||
mock_conn.cursor.return_value = mock_cursor
|
||||
|
||||
with patch.object(PostgresPortfolioRepository, "_connect", return_value=mock_conn):
|
||||
repo.append_asset_tag_id("asset-uuid-1", "tag-uuid-1")
|
||||
|
||||
sql_executed = mock_cursor.execute.call_args_list[0][0][0]
|
||||
assert "NOT (" in sql_executed
|
||||
assert "= ANY(tag_ids)" in sql_executed
|
||||
assert "array_append" in sql_executed
|
||||
|
||||
Reference in New Issue
Block a user