dialog_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. package dialog
  2. import (
  3. stdcontext "context"
  4. "encoding/json"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "strconv"
  9. "strings"
  10. "testing"
  11. "github.com/silenceper/wechat/v2/aispeech/config"
  12. aispeechContext "github.com/silenceper/wechat/v2/aispeech/context"
  13. "github.com/silenceper/wechat/v2/aispeech/encryptor"
  14. "github.com/silenceper/wechat/v2/cache"
  15. )
  16. const (
  17. testAccessToken = "access-token"
  18. testAccount = "admin"
  19. testAESKey = "q1Os1ZMe0nG28KUEx9lg3HjK7V5QyXvi212fzsgDqgz"
  20. testAppID = "appid"
  21. testImportRequest = "import-rid"
  22. testImportTaskID = "task-import"
  23. testPublishTaskID = "task-publish"
  24. testQuery = "hello"
  25. testQueryAnswer = "hello answer"
  26. testQueryRequestID = "query-rid"
  27. testToken = "token"
  28. testTokenRequestID = "token-rid"
  29. )
  30. type emptyAccessTokenHandle struct{}
  31. func (emptyAccessTokenHandle) GetAccessToken() (string, error) {
  32. return "", nil
  33. }
  34. func (emptyAccessTokenHandle) GetAccessTokenContext(ctx stdcontext.Context) (string, error) {
  35. return "", nil
  36. }
  37. func TestAccessTokenCache(t *testing.T) {
  38. var tokenRequests int
  39. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  40. tokenRequests++
  41. body := readBody(t, r)
  42. assertSign(t, r, testToken, body)
  43. if r.Header.Get("X-APPID") != testAppID {
  44. t.Fatalf("bad X-APPID: %s", r.Header.Get("X-APPID"))
  45. }
  46. _, _ = w.Write([]byte(accessTokenResponse("rid")))
  47. }))
  48. defer srv.Close()
  49. ak := NewAccessToken(&config.Config{
  50. AppID: testAppID,
  51. Token: testToken,
  52. Account: testAccount,
  53. BaseURL: srv.URL,
  54. Cache: cache.NewMemory(),
  55. })
  56. token, err := ak.GetAccessToken()
  57. if err != nil {
  58. t.Fatalf("GetAccessToken error: %v", err)
  59. }
  60. if token != testAccessToken {
  61. t.Fatalf("bad token: %s", token)
  62. }
  63. token, err = ak.GetAccessToken()
  64. if err != nil {
  65. t.Fatalf("GetAccessToken second error: %v", err)
  66. }
  67. if token != testAccessToken {
  68. t.Fatalf("bad token second: %s", token)
  69. }
  70. if tokenRequests != 1 {
  71. t.Fatalf("token requests = %d, want 1", tokenRequests)
  72. }
  73. }
  74. func TestAccessTokenCacheByAccount(t *testing.T) {
  75. var tokenRequests int
  76. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  77. tokenRequests++
  78. body := readBody(t, r)
  79. assertSign(t, r, testToken, body)
  80. var req AccessTokenRequest
  81. if err := json.Unmarshal(body, &req); err != nil {
  82. t.Fatalf("bad token body: %v", err)
  83. }
  84. _, _ = w.Write([]byte(accountAccessTokenResponse(req.Account)))
  85. }))
  86. defer srv.Close()
  87. memory := cache.NewMemory()
  88. cfgA := testDialogConfig(srv.URL)
  89. cfgA.Account = "admin-a"
  90. cfgA.Cache = memory
  91. cfgB := testDialogConfig(srv.URL)
  92. cfgB.Account = "admin-b"
  93. cfgB.Cache = memory
  94. tokenA, err := NewAccessToken(cfgA).GetAccessToken()
  95. if err != nil {
  96. t.Fatalf("GetAccessToken A error: %v", err)
  97. }
  98. tokenB, err := NewAccessToken(cfgB).GetAccessToken()
  99. if err != nil {
  100. t.Fatalf("GetAccessToken B error: %v", err)
  101. }
  102. if tokenA != "admin-a-token" || tokenB != "admin-b-token" {
  103. t.Fatalf("bad tokens: %s %s", tokenA, tokenB)
  104. }
  105. if tokenRequests != 2 {
  106. t.Fatalf("token requests = %d, want 2", tokenRequests)
  107. }
  108. }
  109. func TestAccessTokenEmptyToken(t *testing.T) {
  110. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  111. _, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"rid","data":{"access_token":""}}`))
  112. }))
  113. defer srv.Close()
  114. ak := NewAccessToken(&config.Config{
  115. AppID: testAppID,
  116. Token: testToken,
  117. BaseURL: srv.URL,
  118. Cache: cache.NewMemory(),
  119. })
  120. if _, err := ak.GetAccessToken(); err == nil {
  121. t.Fatal("GetAccessToken should return error")
  122. }
  123. }
  124. func TestDialogAPIs(t *testing.T) {
  125. srv := newDialogAPITestServer(t)
  126. defer srv.Close()
  127. d := newTestDialog(srv.URL)
  128. assertDialogImportJSON(t, d)
  129. assertDialogPublish(t, d)
  130. assertDialogProgress(t, d)
  131. assertDialogFetchAsync(t, d)
  132. assertDialogQuery(t, d)
  133. }
  134. func TestDialogEmptyAccessToken(t *testing.T) {
  135. d := NewDialog(&aispeechContext.Context{
  136. Config: &config.Config{
  137. AESKey: testAESKey,
  138. },
  139. AccessTokenContextHandle: emptyAccessTokenHandle{},
  140. })
  141. if _, err := d.ImportJSON(&ImportJSONRequest{}); err == nil {
  142. t.Fatal("ImportJSON should return error")
  143. }
  144. if _, err := d.Publish(); err == nil {
  145. t.Fatal("Publish should return error")
  146. }
  147. if _, err := d.Query(&QueryRequest{}); err == nil {
  148. t.Fatal("Query should return error")
  149. }
  150. }
  151. func TestDialogAPIError(t *testing.T) {
  152. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  153. switch r.URL.Path {
  154. case tokenPath:
  155. _, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
  156. case importJSONPath:
  157. _, _ = w.Write([]byte(`{"code":210202,"msg":"forbidden","request_id":"import-rid","data":{"reason":"forbidden"}}`))
  158. default:
  159. t.Fatalf("unexpected path: %s", r.URL.Path)
  160. }
  161. }))
  162. defer srv.Close()
  163. d := newTestDialog(srv.URL)
  164. _, err := d.ImportJSON(&ImportJSONRequest{})
  165. if err == nil {
  166. t.Fatal("ImportJSON should return error")
  167. }
  168. apiErr, ok := err.(*APIError)
  169. if !ok {
  170. t.Fatalf("ImportJSON error should be *APIError but %T", err)
  171. }
  172. if apiErr.Code != 210202 || apiErr.Msg != "forbidden" || apiErr.RequestID != testImportRequest {
  173. t.Fatalf("bad api error: %+v", apiErr)
  174. }
  175. if string(apiErr.Data) != `{"reason":"forbidden"}` {
  176. t.Fatalf("bad api error data: %s", apiErr.Data)
  177. }
  178. }
  179. func TestQueryPlainJSONError(t *testing.T) {
  180. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  181. switch r.URL.Path {
  182. case tokenPath:
  183. _, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
  184. case queryPath:
  185. _, _ = w.Write([]byte(`{"code":110002,"msg":"bad param","request_id":"query-rid"}`))
  186. default:
  187. t.Fatalf("unexpected path: %s", r.URL.Path)
  188. }
  189. }))
  190. defer srv.Close()
  191. d := newTestDialog(srv.URL)
  192. _, err := d.Query(&QueryRequest{Query: testQuery})
  193. if err == nil {
  194. t.Fatal("Query should return error")
  195. }
  196. apiErr, ok := err.(*APIError)
  197. if !ok {
  198. t.Fatalf("Query error should be *APIError but %T", err)
  199. }
  200. if apiErr.Code != 110002 || apiErr.RequestID != testQueryRequestID {
  201. t.Fatalf("bad api error: %+v", apiErr)
  202. }
  203. }
  204. func newDialogAPITestServer(t *testing.T) *httptest.Server {
  205. t.Helper()
  206. return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  207. body := readBody(t, r)
  208. assertSign(t, r, testToken, body)
  209. switch r.URL.Path {
  210. case tokenPath:
  211. handleDialogToken(t, w, r)
  212. case importJSONPath:
  213. handleDialogImport(t, w, r, body)
  214. case publishPath:
  215. handleDialogPublish(t, w, r, body)
  216. case effectiveProgressPath:
  217. assertToken(t, r)
  218. _, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"progress-rid","data":{"end_time":"","progress":100,"status":1}}`))
  219. case fetchAsyncPath:
  220. assertToken(t, r)
  221. _, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"fetch-rid","data":{"state":2,"msg":"","progress":100,"start":1,"end":2,"url":"","success_skill_info":[{"id":1,"name":"AAA","intents":[{"id":2,"name":"BBB"}]}]}}`))
  222. case queryPath:
  223. handleDialogQuery(t, w, r, body)
  224. default:
  225. t.Fatalf("unexpected path: %s", r.URL.Path)
  226. }
  227. }))
  228. }
  229. func handleDialogToken(t *testing.T, w io.Writer, r *http.Request) {
  230. t.Helper()
  231. if r.Header.Get("X-APPID") != testAppID {
  232. t.Fatalf("bad X-APPID: %s", r.Header.Get("X-APPID"))
  233. }
  234. _, _ = w.Write([]byte(accessTokenResponse(testTokenRequestID)))
  235. }
  236. func handleDialogImport(t *testing.T, w io.Writer, r *http.Request, body []byte) {
  237. t.Helper()
  238. assertToken(t, r)
  239. var req ImportJSONRequest
  240. if err := json.Unmarshal(body, &req); err != nil {
  241. t.Fatalf("bad import body: %v", err)
  242. }
  243. if len(req.Data) != 1 || req.Data[0].Skill != "pre-sale" {
  244. t.Fatalf("bad import request: %+v", req)
  245. }
  246. _, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"import-rid","data":{"task_id":"task-import"}}`))
  247. }
  248. func handleDialogPublish(t *testing.T, w io.Writer, r *http.Request, body []byte) {
  249. t.Helper()
  250. assertToken(t, r)
  251. if len(body) != 0 {
  252. t.Fatalf("publish body should be empty: %s", body)
  253. }
  254. _, _ = w.Write([]byte(`{"code":0,"msg":"success","request_id":"publish-rid","data":{"task_id":"task-publish"}}`))
  255. }
  256. func handleDialogQuery(t *testing.T, w io.Writer, r *http.Request, body []byte) {
  257. t.Helper()
  258. assertToken(t, r)
  259. if !strings.HasPrefix(r.Header.Get("Content-Type"), "text/plain") {
  260. t.Fatalf("bad content type: %s", r.Header.Get("Content-Type"))
  261. }
  262. plain, err := encryptor.Decrypt(testAESKey, string(body))
  263. if err != nil {
  264. t.Fatalf("decrypt query body error: %v", err)
  265. }
  266. var req QueryRequest
  267. if err = json.Unmarshal(plain, &req); err != nil {
  268. t.Fatalf("bad query body: %v", err)
  269. }
  270. if req.Query != testQuery {
  271. t.Fatalf("bad query: %+v", req)
  272. }
  273. _, _ = w.Write([]byte(encryptQueryResponse(t)))
  274. }
  275. func assertDialogImportJSON(t *testing.T, d *Dialog) {
  276. t.Helper()
  277. res, err := d.ImportJSON(&ImportJSONRequest{
  278. Mode: 0,
  279. Data: []BotIntent{{
  280. Skill: "pre-sale",
  281. Intent: "business-hours",
  282. Disable: false,
  283. Questions: []string{"when are you open"},
  284. Answers: []string{"9:00-18:00"},
  285. }},
  286. })
  287. if err != nil || res.TaskID != testImportTaskID || res.RequestID != testImportRequest {
  288. t.Fatalf("ImportJSON = %+v, %v", res, err)
  289. }
  290. }
  291. func assertDialogPublish(t *testing.T, d *Dialog) {
  292. t.Helper()
  293. res, err := d.Publish()
  294. if err != nil || res.TaskID != testPublishTaskID {
  295. t.Fatalf("Publish = %+v, %v", res, err)
  296. }
  297. }
  298. func assertDialogProgress(t *testing.T, d *Dialog) {
  299. t.Helper()
  300. res, err := d.GetEffectiveProgress(&EffectiveProgressRequest{Env: "online"})
  301. if err != nil || res.Progress != 100 {
  302. t.Fatalf("GetEffectiveProgress = %+v, %v", res, err)
  303. }
  304. }
  305. func assertDialogFetchAsync(t *testing.T, d *Dialog) {
  306. t.Helper()
  307. res, err := d.FetchAsync(&FetchAsyncRequest{TaskID: testImportTaskID})
  308. if err != nil || res.State != 2 || len(res.SuccessSkillInfo) != 1 {
  309. t.Fatalf("FetchAsync = %+v, %v", res, err)
  310. }
  311. }
  312. func assertDialogQuery(t *testing.T, d *Dialog) {
  313. t.Helper()
  314. res, err := d.Query(&QueryRequest{Query: testQuery, Env: "online"})
  315. if err != nil || res.Answer != testQueryAnswer || res.RequestID != testQueryRequestID {
  316. t.Fatalf("Query = %+v, %v", res, err)
  317. }
  318. }
  319. func newTestDialog(baseURL string) *Dialog {
  320. cfg := testDialogConfig(baseURL)
  321. return NewDialog(&aispeechContext.Context{
  322. Config: cfg,
  323. AccessTokenContextHandle: NewAccessToken(cfg),
  324. })
  325. }
  326. func testDialogConfig(baseURL string) *config.Config {
  327. return &config.Config{
  328. AppID: testAppID,
  329. Token: testToken,
  330. AESKey: testAESKey,
  331. Account: testAccount,
  332. BaseURL: baseURL,
  333. Cache: cache.NewMemory(),
  334. }
  335. }
  336. func accountAccessTokenResponse(account string) string {
  337. return `{"code":0,"msg":"success","request_id":"rid","data":{"access_token":"` + account + `-token"}}`
  338. }
  339. func accessTokenResponse(requestID string) string {
  340. return `{"code":0,"msg":"success","request_id":"` + requestID + `","data":{"access_token":"` + testAccessToken + `"}}`
  341. }
  342. func encryptQueryResponse(t *testing.T) string {
  343. t.Helper()
  344. cipherText, err := encryptor.Encrypt(testAESKey, []byte(queryResponse()))
  345. if err != nil {
  346. t.Fatalf("encrypt response error: %v", err)
  347. }
  348. return cipherText
  349. }
  350. func queryResponse() string {
  351. return `{"code":0,"msg":"success","request_id":"query-rid","data":{"answer":"hello answer","answer_type":"text","skill_name":"skill","intent_name":"intent","msg_id":"msg","status":"FAQ","slots":[{"name":"n","value":"v","norm":"v"}]}}`
  352. }
  353. func readBody(t *testing.T, r *http.Request) []byte {
  354. t.Helper()
  355. body, err := io.ReadAll(r.Body)
  356. if err != nil {
  357. t.Fatalf("read body error: %v", err)
  358. }
  359. return body
  360. }
  361. func assertToken(t *testing.T, r *http.Request) {
  362. t.Helper()
  363. if r.Header.Get("X-OPENAI-TOKEN") != testAccessToken {
  364. t.Fatalf("bad X-OPENAI-TOKEN: %s", r.Header.Get("X-OPENAI-TOKEN"))
  365. }
  366. }
  367. func assertSign(t *testing.T, r *http.Request, token string, body []byte) {
  368. t.Helper()
  369. timestamp, err := strconv.ParseInt(r.Header.Get("timestamp"), 10, 64)
  370. if err != nil {
  371. t.Fatalf("bad timestamp: %v", err)
  372. }
  373. want := encryptor.Sign(token, timestamp, r.Header.Get("nonce"), body)
  374. if got := r.Header.Get("sign"); got != want {
  375. t.Fatalf("bad sign: got %s want %s body %s", got, want, body)
  376. }
  377. }