Files
Aislo/resources/tester/test_592_1_trash.py
T
eomsangdonandClaude Opus 5.5 ac481a0764 feat(B01): 프로젝트 휴지통 — 지움 = 휴지통 · 7 일 뒤 영구 삭제 · 되살리기 (PLAN 592-1 · 요구 454 · 455)
- 대시보드 지움 = 늘 휴지통(deleted_at) · PROJECT_DELETE_HARD_ENABLED 스위치 걷음 → TRASH_RETENTION_DAYS(7)
- 영구 삭제(hard_delete_project) = 휴지통 것만 · FOR UPDATE · 이미 지운 것 False 로 건너뜀 · 감사 기록 동작 이름 받음
- 정리 루프에 purge_expired_trash — GET_LOCK 이름 잠금(두 PC 서버 중 하나만) · 기한 지난 것 PROJECT_PURGE
- API /api/dashboard/trash/projects — 보기 · 되살리기 = 시스템 관리자(전부) · 회사 관리자(자기 회사) · 영구 삭제 = 시스템 관리자만
- 화면 휴지통 — 시스템 관리 › 휴지통 · 회사 관리 › 휴지통(회사 관리자 · 되살리기만) · 지운 사람 = 감사 기록
- 시험 test_592_1_trash 새로 · test_546 메뉴 기대값에 휴지통 · test_573 복제본 지움을 휴지통 거쳐 영구 삭제로(영구 삭제 = 휴지통 것만)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:31:11 +09:00

124 lines
5.2 KiB
Python

"""592-1 프로젝트 휴지통 지킴 시험.
- 남은 날 = 올림 · 지났으면 0
- 7 일 판정 — 8 일 전 지운 것만 기한 지남 · 6 일 전 · 살아 있는 것은 아님
- 영구 삭제 = 휴지통 것만 · 이미 지운 것은 건너뜀(False) · DB 행 · 폴더 0 · 감사 기록
- 되살리기 = deleted_at 비움
- 정리 잠금 — 남이 잠금을 쥐면 이번 차례 건너뜀
시험 행은 DB 에 바로 넣고(이름 「sub3임시」 · 폴더는 임시 storage) 끝에 행 · 감사 기록째 지움.
"""
import asyncio
import uuid
import pytest
from B01_Dashboard.B01_Dashboard_Repository_Trash import days_left
def test_days_left_rounds_up():
assert [days_left(v) for v in (None, -5, 0, 1, 86400, 86401, 7 * 86400)] == [
0,
0,
0,
1,
1,
2,
7,
]
async def _scenario(storage_root):
from B01_Dashboard.B01_Dashboard_Repository_Trash import restore_project
from common_util.common_util_project_delete import (
_PURGE_LOCK,
hard_delete_project,
list_expired_trash,
purge_expired_trash,
)
from config.config_db import close_db_pool, get_db_pool, init_db_pool
try:
await init_db_pool()
except Exception as error: # DB 없는 PC
pytest.skip(f"DB 연결 안 됨: {error}")
pool = get_db_pool()
ids = {key: str(uuid.uuid4()) for key in ("old", "young", "live")}
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute("SELECT id, company_id FROM users WHERE role = 'SYSTEM_ADMIN' LIMIT 1")
actor, company = await cursor.fetchone()
try:
async with pool.acquire() as connection, connection.cursor() as cursor:
for key, ago in (("old", 8), ("young", 6), ("live", None)):
pid = ids[key]
(storage_root / "c" / "u" / pid).mkdir(parents=True)
(storage_root / "c" / "u" / pid / "x.txt").write_text("x")
await cursor.execute(
"""INSERT INTO projects (id, user_id, company_id, name, storage_path, deleted_at)
VALUES (%s, %s, %s, %s, %s,
IF(%s IS NULL, NULL, CURRENT_TIMESTAMP - INTERVAL %s DAY))""",
(
pid,
actor,
company or 1,
f"sub3임시_592_{key}",
f"storage/c/u/{pid}",
ago,
ago,
),
)
await connection.commit()
expired = {pid for pid, _ in await list_expired_trash(cursor)}
assert ids["old"] in expired
assert ids["young"] not in expired and ids["live"] not in expired
# 남이 잠금을 쥔 동안은 정리 차례를 건너뜀 — 행 그대로
async with pool.acquire() as holder, holder.cursor() as cursor:
await cursor.execute("SELECT GET_LOCK(%s, 0)", (_PURGE_LOCK,))
assert (await cursor.fetchone())[0] == 1
try:
assert await purge_expired_trash() == 0
finally:
await cursor.execute("SELECT RELEASE_LOCK(%s)", (_PURGE_LOCK,))
# 살아 있는 것은 영구 삭제 안 됨
assert await hard_delete_project(ids["live"], actor) is False
assert (storage_root / "c" / "u" / ids["live"]).is_dir()
# 기한 지난 것 영구 삭제 → 행 · 폴더 0 · 다시 부르면 건너뜀
assert await hard_delete_project(ids["old"], actor, action="PROJECT_PURGE") is True
assert await hard_delete_project(ids["old"], actor, action="PROJECT_PURGE") is False
assert not (storage_root / "c" / "u" / ids["old"]).exists()
# 되살리기
assert await restore_project(ids["young"], actor) is True
assert await restore_project(ids["young"], actor) is False
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute(
"SELECT id, deleted_at FROM projects WHERE id IN (%s, %s, %s)", tuple(ids.values())
)
rows = dict(await cursor.fetchall())
await cursor.execute(
"SELECT action FROM system_audit_logs WHERE resource_ref = %s", (ids["old"],)
)
actions = [row[0] for row in await cursor.fetchall()]
assert ids["old"] not in rows
assert rows[ids["young"]] is None
assert actions == ["PROJECT_PURGE"]
finally:
async with pool.acquire() as connection, connection.cursor() as cursor:
marks = ", ".join(["%s"] * len(ids))
await cursor.execute(f"DELETE FROM projects WHERE id IN ({marks})", tuple(ids.values()))
await cursor.execute(
f"DELETE FROM system_audit_logs WHERE resource_ref IN ({marks})",
tuple(ids.values()),
)
await connection.commit()
await close_db_pool()
def test_trash_purge_restore(tmp_path, monkeypatch):
import common_util.common_util_storage as storage
root = tmp_path / "storage"
monkeypatch.setattr(storage, "STORAGE_BASE_DIR", str(root))
asyncio.run(_scenario(root))