- 대시보드 지움 = 늘 휴지통(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>
124 lines
5.2 KiB
Python
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))
|