# backend/tests/test_phase18_features.py — phase-18 신규 기능 # 메일(검색·일괄·라벨·cc) · 일정(반복·알림·RSVP) · 작업(정렬·댓글 수정/삭제) · 반복규칙 변환 from app.connectors.calendar.normalize import classify_event from app.connectors.calendar.recurrence import ( graph_to_rrule, parse_rrule, rrule_to_google, rrule_to_graph, ) from tests._factories import make_comment, make_email, make_mail_account, make_task # ── 일정 카테고리 분류(공휴일/생일/Gmail) ── def test_classify_google_birthday_and_gmail(): assert classify_event("google_calendar", {"eventType": "birthday"}, "축하") == "cat-birthday" assert classify_event("google_calendar", {"eventType": "fromGmail"}, "예약") == "cat-gmail" assert classify_event("google_calendar", {"eventType": "default"}, "주간 회의") is None def test_classify_by_title_heuristic(): assert classify_event("google_calendar", {"eventType": "default"}, "유나 생일") == "cat-birthday" assert classify_event("google_calendar", {"eventType": "default"}, "추석 연휴") == "cat-holiday" def test_classify_outlook_categories(): p = {"categories": ["Holiday"]} assert classify_event("outlook_calendar", p, "Christmas") == "cat-holiday" # ── 반복 규칙(RRULE) 변환: Google/Graph 양방향 ── def test_rrule_to_google_wraps_prefix(): assert rrule_to_google("FREQ=WEEKLY;BYDAY=MO") == ["RRULE:FREQ=WEEKLY;BYDAY=MO"] assert rrule_to_google("") == [] def test_rrule_graph_roundtrip_weekly(): g = rrule_to_graph("FREQ=WEEKLY;BYDAY=MO,WE;INTERVAL=2", "2026-06-15") assert g["pattern"]["type"] == "weekly" assert g["pattern"]["interval"] == 2 assert set(g["pattern"]["daysOfWeek"]) == {"monday", "wednesday"} back = graph_to_rrule(g) r = parse_rrule(back) assert r["FREQ"] == "WEEKLY" and r["INTERVAL"] == "2" assert set(r["BYDAY"]) == {"MO", "WE"} def test_rrule_to_graph_monthly_uses_day_of_month(): g = rrule_to_graph("FREQ=MONTHLY", "2026-06-15") assert g["pattern"]["type"] == "absoluteMonthly" assert g["pattern"]["dayOfMonth"] == 15 def test_rrule_to_graph_count_range(): g = rrule_to_graph("FREQ=DAILY;COUNT=5", "2026-06-15") assert g["range"]["type"] == "numbered" assert g["range"]["numberOfOccurrences"] == 5 # ── 메일 서버측 검색 ── def test_mail_search_filters_by_query(client, session): s, _ = session make_mail_account(s, id="work") make_email(s, id="m1", subject="분기 리포트 검토", preview="리포트 초안") make_email(s, id="m2", subject="점심 약속", preview="강남에서 봐요") hits = client.get("/api/mail?q=리포트").json() ids = {m["id"] for m in hits} assert ids == {"m1"} # ── 메일 일괄 처리 ── def test_mail_bulk_mark_read(client, session): s, _ = session make_mail_account(s, id="work") make_email(s, id="m1", read=False) make_email(s, id="m2", read=False) r = client.post("/api/mail/bulk", json={"ids": ["m1", "m2"], "action": "read"}) assert r.status_code == 200 and r.json()["count"] == 2 rows = {m["id"]: m for m in client.get("/api/mail").json()} assert rows["m1"]["read"] is True and rows["m2"]["read"] is True def test_mail_bulk_trash_removes(client, session): s, _ = session make_mail_account(s, id="work") make_email(s, id="m1") client.post("/api/mail/bulk", json={"ids": ["m1"], "action": "trash"}) assert client.get("/api/mail/m1").status_code == 404 def test_mail_bulk_invalid_action_400(client, session): s, _ = session make_email(s, id="m1") assert client.post("/api/mail/bulk", json={"ids": ["m1"], "action": "nope"}).status_code == 400 # ── 메일 라벨 추가/제거 ── def test_mail_labels_add_remove(client, session): s, _ = session make_mail_account(s, id="work") make_email(s, id="m1", labels=["기존"]) r = client.patch("/api/mail/m1/labels", json={"add": ["중요"], "remove": ["기존"]}) assert r.status_code == 200 assert r.json()["labels"] == ["중요"] # ── 메일 cc 직렬화 ── def test_mail_detail_exposes_cc(client, session): s, _ = session make_mail_account(s, id="work") make_email(s, id="m1") e = client.get("/api/mail/m1").json() assert "cc" in e and "thread_id" in e # ── 일정 생성: 반복 + 알림 (로컬) ── def test_create_event_with_recurrence_and_reminders(client): body = { "title": "주간 회의", "date": "2026-06-17", "start": "10:00", "end": "10:30", "rrule": "FREQ=WEEKLY", "reminders": [10, 30], "people": ["a@b.com"], } ev = client.post("/api/calendar/events", json=body).json() assert ev["rrule"] == "FREQ=WEEKLY" assert ev["reminders"] == [10, 30] assert ev["attendees"][0]["email"] == "a@b.com" # ── 일정 RSVP (로컬) ── def test_event_rsvp_updates_response(client): body = {"title": "초대받은 회의", "date": "2026-06-17", "start": "14:00", "end": "15:00", "people": ["host@x.com"]} ev = client.post("/api/calendar/events", json=body).json() r = client.post(f"/api/calendar/events/{ev['id']}/rsvp", json={"status": "accepted"}) assert r.status_code == 200 assert r.json()["response_status"] == "accepted" def test_event_rsvp_invalid_status_400(client): body = {"title": "회의", "date": "2026-06-17", "start": "14:00", "end": "15:00"} ev = client.post("/api/calendar/events", json=body).json() assert client.post( f"/api/calendar/events/{ev['id']}/rsvp", json={"status": "maybe"} ).status_code == 400 # ── 작업 드래그 정렬 ── def test_task_reorder_sets_sort_order(client, session): s, _ = session make_task(s, id="t1", project_id="me", sort_order=0) make_task(s, id="t2", project_id="me", sort_order=1) make_task(s, id="t3", project_id="me", sort_order=2) r = client.post("/api/tasks/reorder", json={"ids": ["t3", "t1", "t2"]}) assert r.status_code == 200 and r.json()["count"] == 3 order = {t["id"]: t["sort_order"] for t in client.get("/api/tasks").json()} assert order["t3"] == 0 and order["t1"] == 1 and order["t2"] == 2 # ── 댓글 수정/삭제 ── def test_comment_edit_sets_edited_at(client, session): s, _ = session make_task(s, id="t1", project_id="me") make_comment(s, id="c1", task_id="t1", text="원본") r = client.patch("/api/tasks/t1/comments/c1", json={"text": "수정됨"}) assert r.status_code == 200 assert r.json()["text"] == "수정됨" and r.json()["edited_at"] is not None def test_comment_delete(client, session): s, _ = session make_task(s, id="t1", project_id="me") make_comment(s, id="c1", task_id="t1") assert client.delete("/api/tasks/t1/comments/c1").status_code == 200 node = client.get("/api/tasks/t1").json() assert node["comments"] == [] def test_comment_edit_wrong_task_404(client, session): s, _ = session make_task(s, id="t1", project_id="me") make_comment(s, id="c1", task_id="t1") assert client.patch("/api/tasks/t9/comments/c1", json={"text": "x"}).status_code == 404