[feat] solution/backend: 카카오 챗봇 Event API 클라이언트와 테스트 발송 스크립트 (승인 알림 카톡 발송 1단계)
미니블로그 승인 알림을 메일에 더해 연결된 카카오톡으로도 보내기 위한 첫 단계.
승인 방식은 인라인 버튼 + 연결된 계정 신원 확인으로 정했고(링크가 없어 메신저
미리보기가 먼저 열어 승인되는 문제가 없다), 이 커밋은 발송 통로와 규격 확인까지다.
- services/external/kakao_event.py: POST bot-api.kakao.com/v2/bots/{botId}/talk.
예외 문구(str)에는 키·발화자 ID·응답 원문을 넣지 않고 진단용은 detail 에만 둔다
- KAKAO_BOT_REST_API_KEY 를 기존 KAKAO_REST_API_KEY(카카오 로컬 API)와 갈랐다 —
Event API 는 채널을 연결한 비즈니스 인증 앱의 키를 써야 해서 앱이 다를 수 있다
- KAKAO_EVENT_DEV=1 이면 봇 ID 뒤에 "!"(개발 채널). KAKAO_BOT_ID 자체는 웹훅이
bot.id 대조에 쓰므로 고쳐 쓰지 않는다
- scripts/kakao_event_send_test.py: 실제 카톡으로 한 건 보내 규격을 확인하는 스크립트
실제 발송으로 확인함(요청 성공 + 카톡 수신). 이벤트 미배포 시 "Invalid Event name" 404 를
돌려주는 것도 확인했다.
test_kakao_event.py 7건, 카카오·설정 관련 스위트 75 passed
This commit is contained in:
parent
86d870bab5
commit
983c05b3c3
@ -93,7 +93,13 @@ AGENT_CHAT_ENABLED=1
|
||||
# 만드는 법: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
KAKAO_WEBHOOK_SECRET=
|
||||
# 우리 봇이 맞는지 한 겹 더. 오발송을 거르는 용도라 비워도 된다.
|
||||
# ★ 단, Event API(챗봇이 먼저 보내기)를 쓰려면 이 값이 필수다.
|
||||
KAKAO_BOT_ID=
|
||||
# Event API — 채널을 연결한 비즈니스 인증 앱의 REST API 키. 비우면 Event API 는 꺼진다.
|
||||
# ★ 위 KAKAO_REST_API_KEY(카카오 로컬 API)와 다른 값일 수 있어 이름을 갈랐다.
|
||||
KAKAO_BOT_REST_API_KEY=
|
||||
# 1 이면 개발 채널로 보낸다(봇 ID 뒤에 "!"). 운영 채널이면 0.
|
||||
KAKAO_EVENT_DEV=0
|
||||
KAKAO_CHANNEL_PUBLIC_ID=
|
||||
KAKAO_LINK_CODE_TTL_MIN=10
|
||||
KAKAO_LINK_MAX_ATTEMPTS=5
|
||||
|
||||
@ -298,6 +298,12 @@ URL 만 알면 누구나 이 엔드포인트를 때릴 수 있고, `userRequest.
|
||||
★ **바로가기 라벨과 '예' 로 읽는 말이 같아야 한다**(`CONFIRM_LABEL` 등 상수). 어긋나면
|
||||
눌러도 안 먹고, 사장님은 버튼이 고장난 줄 안다.
|
||||
|
||||
## 발행 요청 응답 (2026-09-29)
|
||||
|
||||
발행 작업이 접수되면 **"발행을 시작했습니다. 완료 후 아래 주소에서 확인해 주세요."** 와
|
||||
해당 사이트의 URL을 함께 응답한다. 주소는 `site_payload.publish_url`로 구해 발행 주소와
|
||||
같은 규칙을 쓴다. 완료를 기다리거나 별도 완료 알림을 보내지 않는다 — 이 응답은 접수 안내다.
|
||||
|
||||
## 5초 벽 — 콜백으로 넘는다
|
||||
|
||||
오픈빌더의 스킬 타임아웃은 **5초**다. 넘기면 카카오가 끊어 **말없이 실패하는 봇**이 된다.
|
||||
@ -342,3 +348,68 @@ https://<발행호스트>/v1/agent/kakao/webhook
|
||||
|
||||
★ **채널 '채팅' 과 '챗봇(오픈빌더 스킬)' 은 다른 기능이다.** 채팅만 켜면 발화가 우리에게
|
||||
오지 않는다 — 웹훅이 붙는 쪽은 챗봇이고, 봇을 만들어 채널에 연결해야 한다.
|
||||
|
||||
---
|
||||
|
||||
# 5단계 — 챗봇이 먼저 보내기 (Event API) — 승인 알림을 카톡으로
|
||||
|
||||
메일로 가던 미니블로그 승인 알림을 **연결된 카카오톡으로도** 보낸다(2026-09-29 결정).
|
||||
|
||||
## 결정
|
||||
|
||||
| | 결정 |
|
||||
|---|---|
|
||||
| 발송 | **카톡과 메일 둘 다.** 카톡이 연결돼 있어도 메일을 같이 보낸다 — 카톡 발송은 채널 친구가 아니거나 차단했으면 실패하므로 메일이 누락을 막는다 |
|
||||
| 승인 방식 | 메시지의 **[승인] [수정] 인라인 버튼** + 누른 사람이 **연결된 본인인지·그 글이 본인 가게 것인지 서버가 확인**. 링크가 없어 메신저 미리보기가 먼저 열어 승인되는 문제가 처음부터 없다. 승인은 기존 `PostService.approve_by_owner` 를 그대로 탄다 |
|
||||
| 진행 | **1단계(이 절)**: 클라이언트 + 테스트 발송으로 규격 확인. 2단계: 버튼 응답·승인 처리·발송 연결 |
|
||||
|
||||
## 1단계에서 만든 것
|
||||
|
||||
```
|
||||
services/external/kakao_event.py Event API 클라이언트 — 카카오 계약은 여기 한 곳
|
||||
scripts/kakao_event_send_test.py 테스트 발송(실제 카톡으로 한 건)
|
||||
config/agent_config.py KAKAO_BOT_ID(필수화) · KAKAO_BOT_REST_API_KEY · KAKAO_EVENT_DEV
|
||||
```
|
||||
|
||||
```
|
||||
POST https://bot-api.kakao.com/v2/bots/{botId}/talk (개발 채널이면 botId 뒤에 "!")
|
||||
Authorization: KakaoAK {REST API 키}
|
||||
{"event":{"name":"…","data":{…}}, "user":[{"type":"botUserKey","id":"…"}], "params":{…}}
|
||||
→ {"taskId":"…","status":"SUCCESS", …}
|
||||
```
|
||||
|
||||
- `event.data` 는 말풍선에서 `{{#current.event.data.<이름>}}` 으로, `params` 는 스킬 서버에
|
||||
`userRequest.params` 로 전달된다 — 2단계에서 글 ID 를 `params` 에 실어 보낸다.
|
||||
- 수신자는 `botUserKey` 다. 웹훅의 `userRequest.user.id` 와 같은 값이라
|
||||
`owner_kakao_links.channel_user_key` 를 그대로 쓴다.
|
||||
- ★ **`KAKAO_BOT_REST_API_KEY` 는 기존 `KAKAO_REST_API_KEY`(카카오 로컬 API, 주소 검색)와
|
||||
일부러 갈랐다.** Event API 는 채널을 연결한 비즈니스 인증 앱의 키를 써야 해서 앱이 다를 수 있고,
|
||||
같은 이름이면 한쪽을 채울 때 다른 쪽이 조용히 켜지거나 틀린 키로 나간다.
|
||||
- ★ `KAKAO_EVENT_DEV=1` 로 개발 채널을 가린다. `KAKAO_BOT_ID` 자체에 `!` 를 붙여 쓰지 않는다 —
|
||||
웹훅이 그 값으로 요청의 `bot.id` 를 대조한다.
|
||||
- 예외 문구(`str(ex)`)에는 키·발화자 ID·응답 원문이 없다. 진단용 원문은 `KakaoEventError.detail`.
|
||||
|
||||
## 콘솔에서 먼저 끝내야 하는 것 (코드로 못 한다)
|
||||
|
||||
1. 카카오 디벨로퍼스: 앱 비즈니스 정보 심사 승인 + **카카오톡 채널 연결**(비즈니스 인증 채널과 앱)
|
||||
2. **월렛 생성·연결** — Event API 는 발송 성공 건당 15원(VAT 별도)
|
||||
3. 오픈빌더: 이벤트 정의(예: `post_approval`) → 이벤트 블록의 말풍선에
|
||||
`{{#current.event.data.text}}` → **배포** (배포 전에는 발송되지 않는다)
|
||||
4. `.env`: `KAKAO_BOT_ID` · `KAKAO_BOT_REST_API_KEY` (개발 채널이면 `KAKAO_EVENT_DEV=1`)
|
||||
|
||||
## 한계 (카카오 쪽 제약)
|
||||
|
||||
- 사용자 식별값은 **사용자가 채널에 처음 말을 건 뒤에야 채번된다** — 연결 코드를 보낸 사장님만 받을 수 있다.
|
||||
- 채널 친구가 아니거나 차단했으면 전송은 실패한다 → 메일이 같이 나가는 이유.
|
||||
|
||||
## 테스트 발송
|
||||
|
||||
```bash
|
||||
# botUserKey 는 연결된 사장님의 값이다
|
||||
# SELECT channel_user_key FROM owner_kakao_links WHERE status='LINKED' AND deleted=false;
|
||||
cd solution/backend && .venv/bin/python scripts/kakao_event_send_test.py --key <botUserKey>
|
||||
# 서버(도커)에서는: docker compose exec solution-backend python scripts/kakao_event_send_test.py --key <botUserKey>
|
||||
```
|
||||
|
||||
성공하면 카톡에 이벤트 블록의 말풍선이 뜬다. 요청은 성공인데 안 오면 이벤트 블록 연결 ·
|
||||
배포 · 채널 친구 여부 순서로 본다.
|
||||
|
||||
@ -38,8 +38,18 @@ class AgentConfig(BaseSettings):
|
||||
# 만드는 법: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
KAKAO_WEBHOOK_SECRET: str = ""
|
||||
# 우리 봇이 맞는지 한 겹 더 본다. 시크릿이 아니라 오발송을 거르는 용도라 비워도 된다.
|
||||
# ★ Event API(챗봇이 먼저 보내기)는 이 값이 **필수**다 — 요청 주소에 봇 ID 가 들어간다.
|
||||
KAKAO_BOT_ID: str = ""
|
||||
|
||||
# ★ Event API — 연결된 사장님에게 챗봇이 먼저 말을 거는 통로(services/external/kakao_event.py).
|
||||
# **채널을 연결한 비즈니스 인증 앱**의 REST API 키. 비어 있으면 Event API 는 존재하지 않는다.
|
||||
# ★ 기존 KAKAO_REST_API_KEY(카카오 로컬 API, 주소 검색 어댑터)와 이름을 일부러 갈랐다 —
|
||||
# 앱이 다를 수 있고, 같은 이름이면 한쪽을 채울 때 다른 쪽이 조용히 켜지거나 틀린 키로 나간다.
|
||||
KAKAO_BOT_REST_API_KEY: str = ""
|
||||
# "1" 이면 봇 ID 뒤에 "!" 를 붙여 **개발 채널**로 보낸다(운영 채널과 요청 주소가 다르다).
|
||||
# ★ KAKAO_BOT_ID 자체를 고쳐 쓰지 않는다 — 웹훅이 그 값으로 요청의 bot.id 를 대조한다.
|
||||
KAKAO_EVENT_DEV: str = "0"
|
||||
|
||||
|
||||
def get(name, default=""):
|
||||
return getattr(AgentConfig(), name, default) or default
|
||||
|
||||
51
solution/backend/scripts/kakao_event_send_test.py
Normal file
51
solution/backend/scripts/kakao_event_send_test.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""카카오 챗봇 Event API 테스트 발송 — 규격을 실제로 한 번 확인하는 1회성 스크립트.
|
||||
|
||||
cd solution/backend && .venv/bin/python scripts/kakao_event_send_test.py --key <botUserKey>
|
||||
|
||||
★ 무엇을 확인하나: 콘솔 설정(채널 연결·월렛·이벤트 블록·배포)이 제대로 끝났는지, 그리고
|
||||
요청/응답 모양이 문서와 같은지. 성공하면 카톡에 이벤트 블록의 말풍선이 뜬다.
|
||||
★ botUserKey 는 그 사장님이 채널에 연결 코드를 보낸 뒤 owner_kakao_links.channel_user_key 에
|
||||
저장된 값이다. 아직 연결 전이면 발송할 대상이 없다.
|
||||
SELECT channel_user_key FROM owner_kakao_links WHERE status='LINKED' AND deleted=false;
|
||||
★ 필요한 설정(.env): KAKAO_BOT_ID · KAKAO_BOT_REST_API_KEY (개발 채널이면 KAKAO_EVENT_DEV=1).
|
||||
키·발화자 ID 는 출력하지 않는다.
|
||||
"""
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
os.environ.setdefault("APP_ENV", "local")
|
||||
|
||||
import httpx # noqa: E402
|
||||
|
||||
from services.external import kakao_event # noqa: E402
|
||||
|
||||
|
||||
async def main(key: str, event: str, text: str) -> int:
|
||||
if not kakao_event.is_configured():
|
||||
print("KAKAO_BOT_ID · KAKAO_BOT_REST_API_KEY 가 .env 에 없습니다.")
|
||||
return 1
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
try:
|
||||
task_id = await kakao_event.send(
|
||||
key, event, data={"text": text}, params={"test": "1"}, client=client
|
||||
)
|
||||
except kakao_event.KakaoEventError as ex:
|
||||
print(f"실패 — {ex}")
|
||||
print(f"카카오 응답: {ex.detail}")
|
||||
return 1
|
||||
print(f"요청 성공 — taskId={task_id}")
|
||||
print("카톡에 메시지가 왔는지 확인하세요. 안 오면 이벤트 블록 연결·배포·채널 친구 여부를 봅니다.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--key", required=True, help="수신자 botUserKey")
|
||||
parser.add_argument("--event", default="post_approval", help="오픈빌더에 정의한 이벤트 이름")
|
||||
parser.add_argument("--text", default="Event API 테스트입니다.", help="event.data.text 로 실려 갈 문구")
|
||||
args = parser.parse_args()
|
||||
raise SystemExit(asyncio.run(main(args.key, args.event, args.text)))
|
||||
70
solution/backend/services/external/kakao_event.py
vendored
Normal file
70
solution/backend/services/external/kakao_event.py
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
"""카카오 챗봇 Event API — 연결된 사장님에게 챗봇이 **먼저** 말을 거는 유일한 통로.
|
||||
|
||||
카카오 계약(주소·인증 헤더·요청 모양)은 여기 한 곳에만 둔다. 알림톡 어댑터(alimtalk.py)와
|
||||
같은 규칙이다 — 서비스 계층에 카카오 모양이 새면 채널을 바꿀 때 전부 걷어내야 한다.
|
||||
|
||||
★ 사용자 식별값은 `botUserKey` 다. 오픈빌더 웹훅의 `userRequest.user.id` 와 같은 값이라
|
||||
`owner_kakao_links.channel_user_key` 를 그대로 쓴다. 사용자가 채널에 **처음 말을 건 뒤에야**
|
||||
채번되므로, 연결 코드를 보낸 사장님만 받을 수 있다(카카오 데브톡 답변).
|
||||
★ 채널을 친구 추가하지 않았거나 차단했으면 전송은 실패한다 — 호출부가 다른 경로(메일)로
|
||||
대체할 수 있도록 실패는 예외로 올린다.
|
||||
★ 예외 문구에 REST 키·발화자 ID·응답 원문을 넣지 않는다. 이 문자열은 로그로 간다.
|
||||
진단용 원문은 `KakaoEventError.detail` 에만 담는다.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
|
||||
from config import agent_config as config
|
||||
|
||||
BASE_URL = "https://bot-api.kakao.com/v2/bots"
|
||||
|
||||
|
||||
class KakaoEventError(RuntimeError):
|
||||
"""Event API 호출 실패. `str()` 은 로그에 나가도 되는 코드뿐이고, 원문은 `detail`."""
|
||||
|
||||
def __init__(self, code: str, detail: str = ""):
|
||||
super().__init__(code)
|
||||
self.detail = detail
|
||||
|
||||
|
||||
def is_configured() -> bool:
|
||||
return bool(config.get("KAKAO_BOT_ID") and config.get("KAKAO_BOT_REST_API_KEY"))
|
||||
|
||||
|
||||
def _url() -> str:
|
||||
bot_id = config.get("KAKAO_BOT_ID")
|
||||
if config.get("KAKAO_EVENT_DEV", "0") == "1":
|
||||
bot_id += "!"
|
||||
return f"{BASE_URL}/{bot_id}/talk"
|
||||
|
||||
|
||||
async def send(bot_user_key: str, event_name: str, *, data: dict | None = None,
|
||||
params: dict | None = None, client: httpx.AsyncClient) -> str:
|
||||
"""이벤트 블록을 호출해 그 사용자에게 메시지를 보낸다. 성공하면 taskId.
|
||||
|
||||
`data` 는 말풍선 안에서 `{{#current.event.data.<이름>}}` 으로, `params` 는 스킬 서버에
|
||||
`userRequest.params` 로 전달된다."""
|
||||
if not is_configured():
|
||||
raise KakaoEventError("KAKAO_EVENT_NOT_CONFIGURED")
|
||||
|
||||
event: dict = {"name": event_name}
|
||||
if data:
|
||||
event["data"] = data
|
||||
body: dict = {"event": event, "user": [{"type": "botUserKey", "id": bot_user_key}]}
|
||||
if params:
|
||||
body["params"] = params
|
||||
|
||||
res = await client.post(
|
||||
_url(),
|
||||
headers={"Authorization": f"KakaoAK {config.get('KAKAO_BOT_REST_API_KEY')}"},
|
||||
json=body,
|
||||
)
|
||||
if res.status_code != 200:
|
||||
raise KakaoEventError(f"KAKAO_EVENT_HTTP_{res.status_code}", res.text[:300])
|
||||
try:
|
||||
payload = res.json()
|
||||
except ValueError as ex:
|
||||
raise KakaoEventError("KAKAO_EVENT_INVALID_RESPONSE", res.text[:300]) from ex
|
||||
if payload.get("status") != "SUCCESS":
|
||||
raise KakaoEventError("KAKAO_EVENT_REJECTED", res.text[:300])
|
||||
return str(payload.get("taskId") or "")
|
||||
121
solution/backend/tests/test_kakao_event.py
Normal file
121
solution/backend/tests/test_kakao_event.py
Normal file
@ -0,0 +1,121 @@
|
||||
"""카카오 챗봇 Event API — 연결된 사장님에게 챗봇이 먼저 말을 거는 통로.
|
||||
|
||||
여기서 지키는 것 셋:
|
||||
1. 요청 모양(주소·인증 헤더·본문)이 공식 규격 그대로다 — 어긋나면 카카오가 조용히 거절한다
|
||||
2. 실패는 예외로 올라온다 — 알림이 안 간 것을 성공으로 넘기면 승인 알림이 통째로 사라진다
|
||||
3. 예외 문구에 키·발화자 ID 가 없다 — 이 문자열은 로그로 가고 로그는 우리만 보지 않는다
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from services.external import kakao_event
|
||||
|
||||
BOT_ID = "0123456789abcdef01234567"
|
||||
REST_KEY = "test-rest-api-key-0123456789"
|
||||
SPEAKER = "test-bot-user-key-abc"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def configured(monkeypatch):
|
||||
monkeypatch.setenv("KAKAO_BOT_ID", BOT_ID)
|
||||
monkeypatch.setenv("KAKAO_BOT_REST_API_KEY", REST_KEY)
|
||||
monkeypatch.delenv("KAKAO_EVENT_DEV", raising=False)
|
||||
|
||||
|
||||
def _client(handler) -> httpx.AsyncClient:
|
||||
return httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
||||
|
||||
|
||||
def test_not_configured_without_bot_id_or_rest_key(monkeypatch):
|
||||
assert kakao_event.is_configured() is True
|
||||
monkeypatch.setenv("KAKAO_BOT_REST_API_KEY", "")
|
||||
assert kakao_event.is_configured() is False
|
||||
monkeypatch.setenv("KAKAO_BOT_REST_API_KEY", REST_KEY)
|
||||
monkeypatch.setenv("KAKAO_BOT_ID", "")
|
||||
assert kakao_event.is_configured() is False
|
||||
|
||||
|
||||
async def test_send_posts_the_documented_request_shape():
|
||||
seen = {}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
seen["url"] = str(request.url)
|
||||
seen["auth"] = request.headers["Authorization"]
|
||||
seen["body"] = json.loads(request.content)
|
||||
return httpx.Response(200, json={"taskId": "task-1", "status": "SUCCESS", "message": ""})
|
||||
|
||||
async with _client(handler) as client:
|
||||
task_id = await kakao_event.send(
|
||||
SPEAKER, "post_approval", data={"text": "hello"}, params={"post_id": "p-1"}, client=client
|
||||
)
|
||||
|
||||
assert task_id == "task-1"
|
||||
assert seen["url"] == f"https://bot-api.kakao.com/v2/bots/{BOT_ID}/talk"
|
||||
assert seen["auth"] == f"KakaoAK {REST_KEY}"
|
||||
assert seen["body"] == {
|
||||
"event": {"name": "post_approval", "data": {"text": "hello"}},
|
||||
"user": [{"type": "botUserKey", "id": SPEAKER}],
|
||||
"params": {"post_id": "p-1"},
|
||||
}
|
||||
|
||||
|
||||
async def test_send_omits_data_and_params_when_not_given():
|
||||
seen = {}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
seen["body"] = json.loads(request.content)
|
||||
return httpx.Response(200, json={"taskId": "task-2", "status": "SUCCESS"})
|
||||
|
||||
async with _client(handler) as client:
|
||||
await kakao_event.send(SPEAKER, "post_approval", client=client)
|
||||
|
||||
assert seen["body"]["event"] == {"name": "post_approval"}
|
||||
assert "params" not in seen["body"]
|
||||
|
||||
|
||||
async def test_dev_channel_appends_bang_to_bot_id(monkeypatch):
|
||||
monkeypatch.setenv("KAKAO_EVENT_DEV", "1")
|
||||
seen = {}
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
seen["url"] = str(request.url)
|
||||
return httpx.Response(200, json={"taskId": "task-3", "status": "SUCCESS"})
|
||||
|
||||
async with _client(handler) as client:
|
||||
await kakao_event.send(SPEAKER, "post_approval", client=client)
|
||||
|
||||
assert seen["url"] == f"https://bot-api.kakao.com/v2/bots/{BOT_ID}!/talk"
|
||||
|
||||
|
||||
async def test_http_error_raises_without_leaking_secrets():
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(401, json={"message": "invalid key"})
|
||||
|
||||
async with _client(handler) as client:
|
||||
with pytest.raises(kakao_event.KakaoEventError) as info:
|
||||
await kakao_event.send(SPEAKER, "post_approval", client=client)
|
||||
|
||||
assert "401" in str(info.value)
|
||||
assert REST_KEY not in str(info.value) and SPEAKER not in str(info.value)
|
||||
# 진단용 원문은 str 이 아니라 속성에만 — 로그로 가는 건 str(ex) 뿐이다.
|
||||
assert "invalid key" in info.value.detail
|
||||
|
||||
|
||||
async def test_non_success_status_raises():
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, json={"taskId": "task-4", "status": "FAIL", "message": "no event"})
|
||||
|
||||
async with _client(handler) as client:
|
||||
with pytest.raises(kakao_event.KakaoEventError):
|
||||
await kakao_event.send(SPEAKER, "post_approval", client=client)
|
||||
|
||||
|
||||
async def test_send_refuses_when_not_configured(monkeypatch):
|
||||
monkeypatch.setenv("KAKAO_BOT_REST_API_KEY", "")
|
||||
|
||||
async with _client(lambda request: httpx.Response(200, json={})) as client:
|
||||
with pytest.raises(kakao_event.KakaoEventError, match="NOT_CONFIGURED"):
|
||||
await kakao_event.send(SPEAKER, "post_approval", client=client)
|
||||
Loading…
Reference in New Issue
Block a user