You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
7.1 KiB
Python
186 lines
7.1 KiB
Python
# 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
|