236 lines
5.9 KiB
Go
236 lines
5.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|