@@ -3,10 +3,26 @@ package feishuadapter
33import (
44 "context"
55 "errors"
6+ "net/http"
7+ "net/http/httptest"
8+ "strings"
69 "testing"
710 "time"
811)
912
13+ type failingIngressHandler struct {
14+ messageErr error
15+ cardErr error
16+ }
17+
18+ func (f * failingIngressHandler ) HandleMessage (_ context.Context , _ FeishuMessageEvent ) error {
19+ return f .messageErr
20+ }
21+
22+ func (f * failingIngressHandler ) HandleCardAction (_ context.Context , _ FeishuCardActionEvent ) error {
23+ return f .cardErr
24+ }
25+
1026func TestNewWebhookIngressAndRunContextCancel (t * testing.T ) {
1127 ingress , ok := NewWebhookIngress (Config {
1228 ListenAddress : "127.0.0.1:0" ,
@@ -57,3 +73,144 @@ func TestNewWebhookIngressUsesProvidedClockAndRunReturnsListenError(t *testing.T
5773 t .Fatal ("expected listen error for invalid address" )
5874 }
5975}
76+
77+ func TestWebhookIngressHandleFeishuEventVerificationIgnoreAndHandlerError (t * testing.T ) {
78+ ingress , ok := NewWebhookIngress (Config {
79+ VerifyToken : "verify" ,
80+ SigningSecret : "sign-secret" ,
81+ }, nil ).(* WebhookIngress )
82+ if ! ok {
83+ t .Fatal ("expected webhook ingress instance" )
84+ }
85+
86+ t .Run ("url verification" , func (t * testing.T ) {
87+ request := signedRequest (t , ingress .cfg .SigningSecret , `{"type":"url_verification","challenge":"hello","token":"verify"}` )
88+ recorder := httptest .NewRecorder ()
89+
90+ ingress .handleFeishuEvent (& captureIngressHandler {}).ServeHTTP (recorder , request )
91+
92+ if recorder .Code != http .StatusOK {
93+ t .Fatalf ("status = %d, want %d" , recorder .Code , http .StatusOK )
94+ }
95+ if ! strings .Contains (recorder .Body .String (), `"challenge":"hello"` ) {
96+ t .Fatalf ("response = %s, want challenge body" , recorder .Body .String ())
97+ }
98+ })
99+
100+ t .Run ("unsupported event ignored before token check" , func (t * testing.T ) {
101+ request := signedRequest (t , ingress .cfg .SigningSecret , `{"header":{"event_type":"other"}}` )
102+ recorder := httptest .NewRecorder ()
103+
104+ ingress .handleFeishuEvent (& captureIngressHandler {}).ServeHTTP (recorder , request )
105+
106+ if recorder .Code != http .StatusOK {
107+ t .Fatalf ("status = %d, want %d" , recorder .Code , http .StatusOK )
108+ }
109+ if ! strings .Contains (recorder .Body .String (), `"message":"ignored"` ) {
110+ t .Fatalf ("response = %s, want ignored" , recorder .Body .String ())
111+ }
112+ })
113+
114+ t .Run ("handler error returns retryable response" , func (t * testing.T ) {
115+ body := `{"header":{"event_id":"evt-1","event_type":"im.message.receive_v1","token":"verify"},"event":{"message":{"message_id":"msg-1","chat_id":"chat-1","content":"{\"text\":\"hello\"}"}}}`
116+ request := signedRequest (t , ingress .cfg .SigningSecret , body )
117+ recorder := httptest .NewRecorder ()
118+
119+ ingress .handleFeishuEvent (& failingIngressHandler {messageErr : errors .New ("boom" )}).ServeHTTP (recorder , request )
120+
121+ if recorder .Code != http .StatusInternalServerError {
122+ t .Fatalf ("status = %d, want %d" , recorder .Code , http .StatusInternalServerError )
123+ }
124+ if ! strings .Contains (recorder .Body .String (), "retryable_error" ) {
125+ t .Fatalf ("response = %s, want retryable_error" , recorder .Body .String ())
126+ }
127+ })
128+ }
129+
130+ func TestWebhookIngressHandleCardCallbackActionResponses (t * testing.T ) {
131+ ingress , ok := NewWebhookIngress (Config {
132+ VerifyToken : "verify" ,
133+ SigningSecret : "sign-secret" ,
134+ }, nil ).(* WebhookIngress )
135+ if ! ok {
136+ t .Fatal ("expected webhook ingress instance" )
137+ }
138+
139+ t .Run ("url verification" , func (t * testing.T ) {
140+ request := signedRequest (t , ingress .cfg .SigningSecret , `{"type":"url_verification","challenge":"card-ok","token":"verify","header":{"token":"verify"}}` )
141+ recorder := httptest .NewRecorder ()
142+
143+ ingress .handleCardCallback (& captureIngressHandler {}).ServeHTTP (recorder , request )
144+
145+ if recorder .Code != http .StatusOK {
146+ t .Fatalf ("status = %d, want %d" , recorder .Code , http .StatusOK )
147+ }
148+ if ! strings .Contains (recorder .Body .String (), `"challenge":"card-ok"` ) {
149+ t .Fatalf ("response = %s, want challenge body" , recorder .Body .String ())
150+ }
151+ })
152+
153+ t .Run ("invalid callback returns ready toast" , func (t * testing.T ) {
154+ request := signedRequest (t , ingress .cfg .SigningSecret , `{"action":{"value":{"action_type":"permission","request_id":"perm-1","decision":"allow_all"}},"token":"verify","header":{"token":"verify"}}` )
155+ recorder := httptest .NewRecorder ()
156+
157+ ingress .handleCardCallback (& captureIngressHandler {}).ServeHTTP (recorder , request )
158+
159+ if recorder .Code != http .StatusOK {
160+ t .Fatalf ("status = %d, want %d" , recorder .Code , http .StatusOK )
161+ }
162+ if ! strings .Contains (recorder .Body .String (), "callback ready" ) {
163+ t .Fatalf ("response = %s, want callback ready" , recorder .Body .String ())
164+ }
165+ })
166+
167+ t .Run ("permission success toast" , func (t * testing.T ) {
168+ request := signedRequest (t , ingress .cfg .SigningSecret , `{"action":{"value":{"request_id":"perm-2","decision":"allow_once"}},"token":"verify","header":{"event_id":"evt-perm","token":"verify"}}` )
169+ recorder := httptest .NewRecorder ()
170+ handler := & captureIngressHandler {}
171+
172+ ingress .handleCardCallback (handler ).ServeHTTP (recorder , request )
173+
174+ if recorder .Code != http .StatusOK {
175+ t .Fatalf ("status = %d, want %d" , recorder .Code , http .StatusOK )
176+ }
177+ if len (handler .cards ) != 1 || handler .cards [0 ].Decision != "allow_once" {
178+ t .Fatalf ("unexpected cards: %#v" , handler .cards )
179+ }
180+ if ! strings .Contains (recorder .Body .String (), "审批已提交" ) {
181+ t .Fatalf ("response = %s, want permission toast" , recorder .Body .String ())
182+ }
183+ })
184+
185+ t .Run ("user question success toast" , func (t * testing.T ) {
186+ request := signedRequest (t , ingress .cfg .SigningSecret , `{"action":{"value":{"action_type":"user_question","request_id":"ask-1","status":"answered","value":"A"}},"open_message_id":"card-1","token":"verify","header":{"event_id":"evt-ask","token":"verify"}}` )
187+ recorder := httptest .NewRecorder ()
188+ handler := & captureIngressHandler {}
189+
190+ ingress .handleCardCallback (handler ).ServeHTTP (recorder , request )
191+
192+ if recorder .Code != http .StatusOK {
193+ t .Fatalf ("status = %d, want %d" , recorder .Code , http .StatusOK )
194+ }
195+ if len (handler .cards ) != 1 || handler .cards [0 ].ActionType != "user_question" {
196+ t .Fatalf ("unexpected cards: %#v" , handler .cards )
197+ }
198+ if ! strings .Contains (recorder .Body .String (), "回答已提交" ) {
199+ t .Fatalf ("response = %s, want user question toast" , recorder .Body .String ())
200+ }
201+ })
202+
203+ t .Run ("handler error returns server error" , func (t * testing.T ) {
204+ request := signedRequest (t , ingress .cfg .SigningSecret , `{"action":{"value":{"request_id":"perm-3","decision":"reject"}},"token":"verify","header":{"token":"verify"}}` )
205+ recorder := httptest .NewRecorder ()
206+
207+ ingress .handleCardCallback (& failingIngressHandler {cardErr : errors .New ("boom" )}).ServeHTTP (recorder , request )
208+
209+ if recorder .Code != http .StatusInternalServerError {
210+ t .Fatalf ("status = %d, want %d" , recorder .Code , http .StatusInternalServerError )
211+ }
212+ if ! strings .Contains (recorder .Body .String (), "card action failed" ) {
213+ t .Fatalf ("response = %s, want card action failed" , recorder .Body .String ())
214+ }
215+ })
216+ }
0 commit comments