refactor(qq_onebot): replace all http handler with a brand new websocket handler, seperate with msg recver
This commit is contained in:
1 parent
ba0d8539b3
commit
0c581abc4c
6 files changed
+640
-166
No files matched your search
@@ -0,0 +1,235 @@
|
||||
package qq_onebot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// fakeOneBot is a minimal OneBot 11 forward-WebSocket server. "/api" answers
|
||||
// every action call by echoing its echo with a fixed ok response; "/" accepts the
|
||||
// event socket and hands it to the test so events can be pushed on demand.
|
||||
type fakeOneBot struct {
|
||||
upgrader websocket.Upgrader
|
||||
|
||||
server *httptest.Server
|
||||
apiRequests chan fakeAPIRequest
|
||||
eventConns chan *websocket.Conn
|
||||
}
|
||||
|
||||
type fakeAPIRequest struct {
|
||||
Action string `json:"action"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
Echo json.RawMessage `json:"echo"`
|
||||
}
|
||||
|
||||
func newFakeOneBot(t *testing.T) *fakeOneBot {
|
||||
t.Helper()
|
||||
|
||||
f := &fakeOneBot{
|
||||
apiRequests: make(chan fakeAPIRequest, 16),
|
||||
eventConns: make(chan *websocket.Conn, 16),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := f.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
_, data, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var req fakeAPIRequest
|
||||
if err := json.Unmarshal(data, &req); err != nil {
|
||||
continue
|
||||
}
|
||||
f.apiRequests <- req
|
||||
|
||||
resp, _ := json.Marshal(map[string]json.RawMessage{
|
||||
"status": json.RawMessage(`"ok"`),
|
||||
"retcode": json.RawMessage(`0`),
|
||||
"data": json.RawMessage(`{"message_id":123}`),
|
||||
"echo": req.Echo,
|
||||
})
|
||||
if err := conn.WriteMessage(websocket.TextMessage, resp); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := f.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
f.eventConns <- conn
|
||||
|
||||
// Keep the event socket open until the client goes away.
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
f.server = httptest.NewServer(mux)
|
||||
t.Cleanup(f.server.Close)
|
||||
return f
|
||||
}
|
||||
|
||||
// client returns a Client pointed at this fake server with the proxy disabled.
|
||||
func (f *fakeOneBot) client(t *testing.T) *Client {
|
||||
t.Helper()
|
||||
u, err := url.Parse(f.server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse test server URL: %v", err)
|
||||
}
|
||||
return NewClient(u.Hostname(), u.Port(), "", false)
|
||||
}
|
||||
|
||||
// eventConn waits for the event socket to be established and returns it.
|
||||
func (f *fakeOneBot) eventConn(t *testing.T) *websocket.Conn {
|
||||
t.Helper()
|
||||
select {
|
||||
case conn := <-f.eventConns:
|
||||
return conn
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("event WebSocket never connected")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendMsgOverWebSocket checks an API call is issued over the "/api" socket
|
||||
// and its response is decoded.
|
||||
func TestSendMsgOverWebSocket(t *testing.T) {
|
||||
f := newFakeOneBot(t)
|
||||
|
||||
c := f.client(t)
|
||||
go c.Listen(func([]byte) {})
|
||||
defer c.Stop()
|
||||
|
||||
resp, err := c.SendMsg("group", 12345, "hello", false, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("SendMsg: %v", err)
|
||||
}
|
||||
if resp.Status != "ok" || resp.RetCode != 0 {
|
||||
t.Fatalf("unexpected response: %+v", resp)
|
||||
}
|
||||
|
||||
var data struct {
|
||||
MessageID int64 `json:"message_id"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Data, &data); err != nil {
|
||||
t.Fatalf("decode data: %v", err)
|
||||
}
|
||||
if data.MessageID != 123 {
|
||||
t.Fatalf("message_id = %d, want 123", data.MessageID)
|
||||
}
|
||||
|
||||
req := <-f.apiRequests
|
||||
if req.Action != "send_group_msg" {
|
||||
t.Fatalf("action = %q, want send_group_msg", req.Action)
|
||||
}
|
||||
var params map[string]any
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
t.Fatalf("decode params: %v", err)
|
||||
}
|
||||
if params["group_id"].(float64) != 12345 || params["message"].(string) != "hello" {
|
||||
t.Fatalf("params = %v", params)
|
||||
}
|
||||
var echo string
|
||||
if err := json.Unmarshal(req.Echo, &echo); err != nil {
|
||||
t.Fatalf("echo is not a JSON string: %s", req.Echo)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBodylessActionSendsEmptyParams checks actions without arguments still send
|
||||
// a params object (never null), which some adapters reject.
|
||||
func TestBodylessActionSendsEmptyParams(t *testing.T) {
|
||||
f := newFakeOneBot(t)
|
||||
|
||||
c := f.client(t)
|
||||
go c.Listen(func([]byte) {})
|
||||
defer c.Stop()
|
||||
|
||||
if _, err := c.GetGroupList(); err != nil {
|
||||
t.Fatalf("GetGroupList: %v", err)
|
||||
}
|
||||
|
||||
req := <-f.apiRequests
|
||||
if req.Action != "get_group_list" {
|
||||
t.Fatalf("action = %q, want get_group_list", req.Action)
|
||||
}
|
||||
if string(req.Params) != "{}" {
|
||||
t.Fatalf("params = %s, want {}", req.Params)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendMsgFromEventCallbackDoesNotDeadlock is the regression test for the
|
||||
// reason the client uses two sockets: the event handler replies with SendMsg from
|
||||
// inside the event read loop, so a response read on that same connection would
|
||||
// never arrive.
|
||||
func TestSendMsgFromEventCallbackDoesNotDeadlock(t *testing.T) {
|
||||
f := newFakeOneBot(t)
|
||||
|
||||
c := f.client(t)
|
||||
|
||||
type result struct {
|
||||
resp *APIResponse
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go c.Listen(func(raw []byte) {
|
||||
resp, err := c.SendMsg("group", 1, "pong", false, 0)
|
||||
done <- result{resp, err}
|
||||
})
|
||||
defer c.Stop()
|
||||
|
||||
evConn := f.eventConn(t)
|
||||
event := []byte(`{"post_type":"message","message_type":"group","group_id":1,"user_id":2,"raw_message":"/ping","self_id":9}`)
|
||||
if err := evConn.WriteMessage(websocket.TextMessage, event); err != nil {
|
||||
t.Fatalf("write event: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case r := <-done:
|
||||
if r.err != nil {
|
||||
t.Fatalf("SendMsg from event callback: %v", r.err)
|
||||
}
|
||||
if r.resp.Status != "ok" {
|
||||
t.Fatalf("unexpected response: %+v", r.resp)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("deadlock: SendMsg did not return from within the event callback")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEchoKey covers the response-matcher: an absent or null echo is not a
|
||||
// response, and both string and non-string echoes normalize to a key.
|
||||
func TestEchoKey(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
key string
|
||||
ok bool
|
||||
}{
|
||||
{`"42"`, "42", true},
|
||||
{`42`, "42", true},
|
||||
{`null`, "", false},
|
||||
{``, "", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
key, ok := echoKey(json.RawMessage(tc.in))
|
||||
if key != tc.key || ok != tc.ok {
|
||||
t.Errorf("echoKey(%q) = (%q, %v), want (%q, %v)", tc.in, key, ok, tc.key, tc.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user