"""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))