Files
NixMsg/sdk/go/itest_checklist_test.go
T

646 lines
21 KiB
Go

package nixmsg_test
import (
"context"
"errors"
"strings"
"sync/atomic"
"testing"
"time"
nixmsg "git.asio.asia/nixevol/NixMsg/sdk/go"
)
const (
itestPass = "password12"
itestCode = "sdk-go-code1"
)
func TestChecklistAgainstRealServer(t *testing.T) {
srv := startITestServer(t)
admin := newAdminHTTP(t, srv.AdminHTTPBase, srv.AdminPassword)
admin.putRegistration(t, true, itestCode)
ctx := context.Background()
ws := srv.MQTTWS
t.Run("01_handshake_limits", func(t *testing.T) {
mustRegister(t, ctx, ws, "go01a", "A")
c := mustConnect(t, ctx, ws, "go01a", itestPass)
defer c.Close()
lim := c.Limits()
if lim.MaxBodyBytes <= 0 || lim.MaxFrameBytes <= 0 || lim.ServerTimeMs <= 0 {
t.Fatalf("handshake limits incomplete: %+v", lim)
}
if lim.MaxBodyBytes != 262144 {
t.Fatalf("max_body_bytes=%d want 262144", lim.MaxBodyBytes)
}
var tok string
c2 := nixmsg.New()
c2.OnSession(func(token string) { tok = token })
if err := c2.Connect(ctx, ws, "go01a", nixmsg.Credential{Password: itestPass}, nixmsg.Options{ConnectTimeout: 20 * time.Second}); err != nil {
t.Fatal(err)
}
defer c2.Close()
if tok == "" || !strings.HasPrefix(tok, "nst_") {
t.Fatalf("session token=%q", tok)
}
})
t.Run("02_dm_callback_once", func(t *testing.T) {
mustRegister(t, ctx, ws, "go02a", "A")
mustRegister(t, ctx, ws, "go02b", "B")
a := mustConnect(t, ctx, ws, "go02a", itestPass)
defer a.Close()
b := mustConnect(t, ctx, ws, "go02b", itestPass)
defer b.Close()
var n atomic.Int32
got := make(chan nixmsg.Message, 4)
b.OnMessage(func(msg nixmsg.Message) error {
n.Add(1)
got <- msg
return nil
})
res, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go02b"}, nixmsg.Body{Enc: "utf8", Data: "hi-once"}, nixmsg.SendOptions{Delay: delay0()})
if err != nil {
t.Fatal(err)
}
msg := waitMsg(t, got, 8*time.Second)
if msg.ID != res.ID || msg.From != "go02a" || msg.Body.Data != "hi-once" {
t.Fatalf("msg=%+v res=%+v", msg, res)
}
time.Sleep(500 * time.Millisecond)
if n.Load() != 1 {
t.Fatalf("callbacks=%d want 1", n.Load())
}
})
t.Run("03_send_while_disconnected", func(t *testing.T) {
mustRegister(t, ctx, ws, "go03a", "A")
mustRegister(t, ctx, ws, "go03b", "B")
a := mustConnect(t, ctx, ws, "go03a", itestPass)
defer a.Close()
b := mustConnect(t, ctx, ws, "go03b", itestPass)
defer b.Close()
got := make(chan nixmsg.Message, 4)
var n atomic.Int32
b.OnMessage(func(msg nixmsg.Message) error {
n.Add(1)
got <- msg
return nil
})
states := make(chan nixmsg.ConnectionState, 16)
a.OnConnection(func(ev nixmsg.ConnectionEvent) { states <- ev.State })
admin.kick(t, "go03a")
saw := waitConnState(t, states, 15*time.Second, nixmsg.StateReconnecting, nixmsg.StateOnline)
resCh := make(chan nixmsg.SendResult, 1)
errCh := make(chan error, 1)
go func() {
res, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go03b"}, nixmsg.Body{Enc: "utf8", Data: "queued"}, nixmsg.SendOptions{Delay: delay0(), Keep: true, ID: "go03-msg-1"})
if err != nil {
errCh <- err
return
}
resCh <- res
}()
if saw != nixmsg.StateOnline {
_ = waitConnState(t, states, 30*time.Second, nixmsg.StateOnline)
}
select {
case err := <-errCh:
t.Fatal(err)
case res := <-resCh:
if res.ID != "go03-msg-1" {
t.Fatalf("id=%s", res.ID)
}
case <-time.After(30 * time.Second):
t.Fatal("send timeout")
}
msg := waitMsg(t, got, 15*time.Second)
if msg.ID != "go03-msg-1" {
t.Fatalf("msg=%+v", msg)
}
time.Sleep(800 * time.Millisecond)
if n.Load() != 1 {
t.Fatalf("callbacks=%d want 1", n.Load())
}
})
t.Run("04_same_id_retry_and_dedup", func(t *testing.T) {
mustRegister(t, ctx, ws, "go04a", "A")
mustRegister(t, ctx, ws, "go04b", "B")
a := mustConnect(t, ctx, ws, "go04a", itestPass)
defer a.Close()
b := mustConnect(t, ctx, ws, "go04b", itestPass)
defer b.Close()
// 同号:断线期间入队发送,重连后以同一消息号送达
var n atomic.Int32
got := make(chan nixmsg.Message, 4)
b.OnMessage(func(msg nixmsg.Message) error {
n.Add(1)
got <- msg
return nil
})
states := make(chan nixmsg.ConnectionState, 16)
a.OnConnection(func(ev nixmsg.ConnectionEvent) { states <- ev.State })
admin.kick(t, "go04a")
saw := waitConnState(t, states, 15*time.Second, nixmsg.StateReconnecting, nixmsg.StateOnline)
go func() {
_, _ = a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go04b"}, nixmsg.Body{Enc: "utf8", Data: "same-id"}, nixmsg.SendOptions{Delay: delay0(), Keep: true, ID: "go04-fixed-id"})
}()
if saw != nixmsg.StateOnline {
_ = waitConnState(t, states, 30*time.Second, nixmsg.StateOnline)
}
msg := waitMsg(t, got, 15*time.Second)
if msg.ID != "go04-fixed-id" {
t.Fatalf("want go04-fixed-id got %s", msg.ID)
}
time.Sleep(500 * time.Millisecond)
if n.Load() != 1 {
t.Fatalf("same-id callbacks=%d", n.Load())
}
// 模拟未确认:手动确认模式收一次、踢线重连后服务器重推,SDK 不重复回调;再手动 ack
b2 := nixmsg.New()
var n2 atomic.Int32
got2 := make(chan nixmsg.Message, 4)
b2.OnMessage(func(msg nixmsg.Message) error {
n2.Add(1)
got2 <- msg
return nil
})
if err := b2.Connect(ctx, ws, "go04b", nixmsg.Credential{Password: itestPass}, nixmsg.Options{ConnectTimeout: 20 * time.Second, ManualAck: true}); err != nil {
t.Fatal(err)
}
defer b2.Close()
_, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go04b"}, nixmsg.Body{Enc: "utf8", Data: "repush"}, nixmsg.SendOptions{Delay: delay0(), Keep: true, TTL: int64Ptr(3600), ID: "go04-repush"})
if err != nil {
t.Fatal(err)
}
m1 := waitMsg(t, got2, 10*time.Second)
if m1.ID != "go04-repush" {
t.Fatalf("m1=%+v", m1)
}
st2 := make(chan nixmsg.ConnectionState, 16)
b2.OnConnection(func(ev nixmsg.ConnectionEvent) { st2 <- ev.State })
admin.kick(t, "go04b")
_ = waitConnState(t, st2, 30*time.Second, nixmsg.StateOnline)
time.Sleep(2 * time.Second)
if n2.Load() != 1 {
t.Fatalf("repush callbacks=%d want 1 (no duplicate)", n2.Load())
}
if err := b2.Ack(m1); err != nil {
t.Fatal(err)
}
})
t.Run("05_recall_within_delay", func(t *testing.T) {
mustRegister(t, ctx, ws, "go05a", "A")
mustRegister(t, ctx, ws, "go05b", "B")
a := mustConnect(t, ctx, ws, "go05a", itestPass)
defer a.Close()
b := mustConnect(t, ctx, ws, "go05b", itestPass)
defer b.Close()
var msgN, revN atomic.Int32
b.OnMessage(func(msg nixmsg.Message) error {
msgN.Add(1)
return nil
})
b.OnRevoked(func(e nixmsg.RevokedEvent) { revN.Add(1) })
d := 10 * time.Second
res, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go05b"}, nixmsg.Body{Enc: "utf8", Data: "will-recall"}, nixmsg.SendOptions{Delay: &d, ID: "go05-rec"})
if err != nil {
t.Fatal(err)
}
if res.State != "scheduled" && res.State != "" {
// state may be scheduled
}
if _, err := a.Recall(ctx, "go05-rec"); err != nil {
t.Fatal(err)
}
time.Sleep(1500 * time.Millisecond)
if msgN.Load() != 0 {
t.Fatalf("bob got msg callbacks=%d", msgN.Load())
}
if revN.Load() != 0 {
t.Fatalf("bob got revoked=%d (undelivered should be silent)", revN.Load())
}
})
t.Run("06_scheduled_about_2s", func(t *testing.T) {
mustRegister(t, ctx, ws, "go06a", "A")
mustRegister(t, ctx, ws, "go06b", "B")
a := mustConnect(t, ctx, ws, "go06a", itestPass)
defer a.Close()
b := mustConnect(t, ctx, ws, "go06b", itestPass)
defer b.Close()
got := make(chan time.Time, 1)
b.OnMessage(func(msg nixmsg.Message) error {
got <- time.Now()
return nil
})
d := 2 * time.Second
start := time.Now()
if _, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go06b"}, nixmsg.Body{Enc: "utf8", Data: "later"}, nixmsg.SendOptions{Delay: &d}); err != nil {
t.Fatal(err)
}
select {
case at := <-got:
elapsed := at.Sub(start)
if elapsed < 1500*time.Millisecond || elapsed > 6*time.Second {
t.Fatalf("elapsed=%v want ~2s", elapsed)
}
case <-time.After(10 * time.Second):
t.Fatal("timeout waiting scheduled msg")
}
})
t.Run("07_offline_keep", func(t *testing.T) {
mustRegister(t, ctx, ws, "go07a", "A")
mustRegister(t, ctx, ws, "go07b", "B")
mustRegister(t, ctx, ws, "go07c", "C")
a := mustConnect(t, ctx, ws, "go07a", itestPass)
defer a.Close()
// 晚 1 秒上线能收到
ttl := int64(60)
if _, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go07b"}, nixmsg.Body{Enc: "utf8", Data: "keep-ok"}, nixmsg.SendOptions{Delay: delay0(), Keep: true, TTL: &ttl, ID: "go07-keep-ok"}); err != nil {
t.Fatal(err)
}
time.Sleep(1 * time.Second)
got := make(chan nixmsg.Message, 2)
b := nixmsg.New()
b.OnMessage(func(msg nixmsg.Message) error {
got <- msg
return nil
})
if err := b.Connect(ctx, ws, "go07b", nixmsg.Credential{Password: itestPass}, nixmsg.Options{ConnectTimeout: 20 * time.Second}); err != nil {
t.Fatal(err)
}
defer b.Close()
msg := waitMsg(t, got, 10*time.Second)
if msg.ID != "go07-keep-ok" {
t.Fatalf("keep-ok msg=%+v", msg)
}
// 保留 1 秒,3 秒后上线收不到;发送方收到过期回执
receiptCh := make(chan nixmsg.Receipt, 4)
a.OnReceipt(func(r nixmsg.Receipt) { receiptCh <- r })
ttl1 := int64(1)
if _, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go07c"}, nixmsg.Body{Enc: "utf8", Data: "expire"}, nixmsg.SendOptions{Delay: delay0(), Keep: true, TTL: &ttl1, ID: "go07-exp", Receipt: boolPtr(true)}); err != nil {
t.Fatal(err)
}
time.Sleep(3 * time.Second)
c := mustConnect(t, ctx, ws, "go07c", itestPass)
defer c.Close()
var cN atomic.Int32
c.OnMessage(func(msg nixmsg.Message) error {
cN.Add(1)
return nil
})
time.Sleep(2 * time.Second)
if cN.Load() != 0 {
t.Fatalf("expired offline should not deliver, got %d", cN.Load())
}
deadline := time.Now().Add(15 * time.Second)
for time.Now().Before(deadline) {
select {
case r := <-receiptCh:
if r.ID == "go07-exp" && (r.State == "expired" || r.Reason == "expired" || strings.Contains(r.State, "expir") || strings.Contains(r.Reason, "expir")) {
return
}
case <-time.After(200 * time.Millisecond):
}
}
// 回执可能稍慢;再扫一下
select {
case r := <-receiptCh:
if r.ID != "go07-exp" {
t.Fatalf("unexpected receipt %+v", r)
}
default:
t.Fatal("未收到过期回执")
}
})
t.Run("08_group_no_echo_to_sender", func(t *testing.T) {
mustRegister(t, ctx, ws, "go08a", "A")
mustRegister(t, ctx, ws, "go08b", "B")
mustRegister(t, ctx, ws, "go08c", "C")
a := mustConnect(t, ctx, ws, "go08a", itestPass)
defer a.Close()
b := mustConnect(t, ctx, ws, "go08b", itestPass)
defer b.Close()
c := mustConnect(t, ctx, ws, "go08c", itestPass)
defer c.Close()
if _, err := a.CreateGroup(ctx, "g_go08", "G8", []nixmsg.GroupMemberIn{{ID: "go08b"}, {ID: "go08c"}}); err != nil {
t.Fatal(err)
}
time.Sleep(300 * time.Millisecond)
bGot := make(chan nixmsg.Message, 2)
cGot := make(chan nixmsg.Message, 2)
var aN atomic.Int32
a.OnMessage(func(msg nixmsg.Message) error {
aN.Add(1)
return nil
})
b.OnMessage(func(msg nixmsg.Message) error {
bGot <- msg
return nil
})
c.OnMessage(func(msg nixmsg.Message) error {
cGot <- msg
return nil
})
if _, err := a.Send(ctx, nixmsg.Target{Kind: "group", ID: "g_go08"}, nixmsg.Body{Enc: "utf8", Data: "ghi"}, nixmsg.SendOptions{Delay: delay0(), ID: "go08-g1"}); err != nil {
t.Fatal(err)
}
mb := waitMsg(t, bGot, 10*time.Second)
mc := waitMsg(t, cGot, 10*time.Second)
if mb.Body.Data != "ghi" || mc.Body.Data != "ghi" {
t.Fatalf("b=%+v c=%+v", mb, mc)
}
time.Sleep(500 * time.Millisecond)
if aN.Load() != 0 {
t.Fatalf("sender got %d group msgs", aN.Load())
}
})
t.Run("09_talk_password", func(t *testing.T) {
mustRegister(t, ctx, ws, "go09a", "A")
mustRegister(t, ctx, ws, "go09b", "B")
a := mustConnect(t, ctx, ws, "go09a", itestPass)
defer a.Close()
b := mustConnect(t, ctx, ws, "go09b", itestPass)
defer b.Close()
if err := b.SetTalkPassword(ctx, "talk9"); err != nil {
t.Fatal(err)
}
_, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go09b"}, nixmsg.Body{Enc: "utf8", Data: "x"}, nixmsg.SendOptions{Delay: delay0()})
if err == nil {
t.Fatal("want talk_password_required")
}
var ae *nixmsg.APIError
if !errors.As(err, &ae) || ae.Code != "talk_password_required" {
t.Fatalf("err=%v", err)
}
if err := a.Unlock(ctx, "go09b", "talk9"); err != nil {
t.Fatal(err)
}
if _, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go09b"}, nixmsg.Body{Enc: "utf8", Data: "ok"}, nixmsg.SendOptions{Delay: delay0()}); err != nil {
t.Fatal(err)
}
if err := b.SetTalkPassword(ctx, "talk9b"); err != nil {
t.Fatal(err)
}
_, err = a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go09b"}, nixmsg.Body{Enc: "utf8", Data: "stale"}, nixmsg.SendOptions{Delay: delay0()})
if err == nil {
t.Fatal("want auth invalid after password change")
}
if !errors.As(err, &ae) || (ae.Code != "talk_password_required" && ae.Code != "talk_password_invalid") {
t.Fatalf("after change err=%v", err)
}
// 对方先发则可回复:b 给 a 发(a 无对话密码)后,a 可回 b(即使用旧授权已失效,回复授权由 b→a 的发送产生)
got := make(chan nixmsg.Message, 2)
a.OnMessage(func(msg nixmsg.Message) error {
got <- msg
return nil
})
if _, err := b.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go09a"}, nixmsg.Body{Enc: "utf8", Data: "first"}, nixmsg.SendOptions{Delay: delay0()}); err != nil {
t.Fatal(err)
}
_ = waitMsg(t, got, 8*time.Second)
if _, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go09b"}, nixmsg.Body{Enc: "utf8", Data: "reply"}, nixmsg.SendOptions{Delay: delay0()}); err != nil {
t.Fatalf("reply after peer first send: %v", err)
}
})
t.Run("10_second_login_kicks_first", func(t *testing.T) {
mustRegister(t, ctx, ws, "go10a", "A")
c1 := mustConnect(t, ctx, ws, "go10a", itestPass)
defer c1.Close()
var kicked atomic.Bool
c1.OnConnection(func(ev nixmsg.ConnectionEvent) {
if ev.State == nixmsg.StateKicked {
kicked.Store(true)
}
})
c2 := mustConnect(t, ctx, ws, "go10a", itestPass)
defer c2.Close()
deadline := time.Now().Add(15 * time.Second)
for time.Now().Before(deadline) {
if kicked.Load() {
break
}
time.Sleep(50 * time.Millisecond)
}
if !kicked.Load() {
t.Fatal("first client not kicked")
}
time.Sleep(2 * time.Second)
// 被顶号后不应恢复 online
if c1.Limits().ServerTimeMs != 0 {
// Limits 仍可能保留旧值;用连接状态回调或再次 Send 探测
}
_, err := c1.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go10a"}, nixmsg.Body{Enc: "utf8", Data: "x"}, nixmsg.SendOptions{Delay: delay0()})
if err == nil {
t.Fatal("kicked client should not send successfully after stop-reconnect")
}
})
t.Run("11_body_too_large_local", func(t *testing.T) {
mustRegister(t, ctx, ws, "go11a", "A")
mustRegister(t, ctx, ws, "go11b", "B")
a := mustConnect(t, ctx, ws, "go11a", itestPass)
defer a.Close()
big := strings.Repeat("x", 262144+1)
_, err := a.Send(ctx, nixmsg.Target{Kind: "endpoint", ID: "go11b"}, nixmsg.Body{Enc: "utf8", Data: big}, nixmsg.SendOptions{Delay: delay0()})
if err == nil {
t.Fatal("want body_too_large")
}
var ae *nixmsg.APIError
if !errors.As(err, &ae) || ae.Code != nixmsg.CodeBodyTooLarge {
t.Fatalf("err=%v", err)
}
})
t.Run("12_registration_switch_and_code", func(t *testing.T) {
admin.putRegistration(t, false, itestCode)
_, err := nixmsg.Register(ctx, ws, itestCode, nixmsg.RegisterOptions{ID: "go12x", LoginPassword: itestPass, Name: "X"})
if err == nil {
t.Fatal("want registration closed")
}
admin.putRegistration(t, true, itestCode)
_, err = nixmsg.Register(ctx, ws, "wrong-code-xx", nixmsg.RegisterOptions{ID: "go12y", LoginPassword: itestPass, Name: "Y"})
if err == nil {
t.Fatal("want bad code")
}
codeNew := "sdk-go-code2"
admin.putRegistration(t, true, itestCode)
res, err := nixmsg.Register(ctx, ws, itestCode, nixmsg.RegisterOptions{ID: "go12ok", LoginPassword: itestPass, Name: "OK"})
if err != nil {
t.Fatal(err)
}
if res.ID != "go12ok" {
t.Fatalf("id=%s", res.ID)
}
c := mustConnect(t, ctx, ws, "go12ok", itestPass)
c.Close()
admin.putRegistration(t, true, codeNew)
_, err = nixmsg.Register(ctx, ws, itestCode, nixmsg.RegisterOptions{ID: "go12old", LoginPassword: itestPass, Name: "Old"})
if err == nil {
t.Fatal("old code should fail")
}
// 已注册端仍可用旧密码登录
c2 := mustConnect(t, ctx, ws, "go12ok", itestPass)
c2.Close()
admin.putRegistration(t, true, itestCode) // 恢复供后续子测试
})
t.Run("13_change_login_password", func(t *testing.T) {
mustRegister(t, ctx, ws, "go13a", "A")
c := mustConnect(t, ctx, ws, "go13a", itestPass)
if err := c.ChangeLoginPassword(ctx, itestPass, "password99"); err != nil {
t.Fatal(err)
}
_ = c.Close()
cNew := mustConnect(t, ctx, ws, "go13a", "password99")
cNew.Close()
cBad := nixmsg.New()
var authFail atomic.Bool
cBad.OnConnection(func(ev nixmsg.ConnectionEvent) {
if ev.State == nixmsg.StateAuthFailed {
authFail.Store(true)
}
})
err := cBad.Connect(ctx, ws, "go13a", nixmsg.Credential{Password: itestPass}, nixmsg.Options{ConnectTimeout: 10 * time.Second})
if err == nil {
_ = cBad.Close()
t.Fatal("old password should fail")
}
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) && !authFail.Load() {
time.Sleep(50 * time.Millisecond)
}
time.Sleep(1500 * time.Millisecond)
// 不应恢复为 online
if !authFail.Load() {
t.Log("auth_failed event not observed; connect error present which is enough")
}
_ = cBad.Close()
})
t.Run("15_session_token", func(t *testing.T) {
mustRegister(t, ctx, ws, "go15a", "A")
var tok1 string
c1 := nixmsg.New()
c1.OnSession(func(token string) { tok1 = token })
if err := c1.Connect(ctx, ws, "go15a", nixmsg.Credential{Password: itestPass}, nixmsg.Options{ConnectTimeout: 20 * time.Second}); err != nil {
t.Fatal(err)
}
if tok1 == "" {
t.Fatal("no token")
}
_ = c1.Close()
cTok := nixmsg.New()
if err := cTok.Connect(ctx, ws, "go15a", nixmsg.Credential{SessionToken: tok1}, nixmsg.Options{ConnectTimeout: 20 * time.Second}); err != nil {
t.Fatalf("token reconnect: %v", err)
}
_ = cTok.Close()
// 另一处密码登录使旧令牌失效
cPass := mustConnect(t, ctx, ws, "go15a", itestPass)
defer cPass.Close()
cOld := nixmsg.New()
var inv atomic.Bool
cOld.OnConnection(func(ev nixmsg.ConnectionEvent) {
if ev.State == nixmsg.StateAuthFailed && (ev.Reason == string(nixmsg.AuthSessionInvalid) || strings.Contains(ev.Reason, "session")) {
inv.Store(true)
}
if ev.State == nixmsg.StateAuthFailed {
inv.Store(true)
}
})
err := cOld.Connect(ctx, ws, "go15a", nixmsg.Credential{SessionToken: tok1}, nixmsg.Options{ConnectTimeout: 10 * time.Second})
if err == nil {
_ = cOld.Close()
t.Fatal("old token should fail after other password login")
}
_ = cOld.Close()
// logout 后令牌失效
var tok2 string
c3 := nixmsg.New()
c3.OnSession(func(token string) { tok2 = token })
if err := c3.Connect(ctx, ws, "go15a", nixmsg.Credential{Password: itestPass}, nixmsg.Options{ConnectTimeout: 20 * time.Second}); err != nil {
t.Fatal(err)
}
if err := c3.Logout(ctx); err != nil {
t.Fatal(err)
}
_ = c3.Close()
c4 := nixmsg.New()
err = c4.Connect(ctx, ws, "go15a", nixmsg.Credential{SessionToken: tok2}, nixmsg.Options{ConnectTimeout: 10 * time.Second})
if err == nil {
_ = c4.Close()
t.Fatal("token after logout should fail")
}
_ = c4.Close()
})
}
func mustRegister(t *testing.T, ctx context.Context, ws, id, name string) {
t.Helper()
_, err := nixmsg.Register(ctx, ws, itestCode, nixmsg.RegisterOptions{ID: id, LoginPassword: itestPass, Name: name})
if err != nil {
// 可能已存在(重跑);尝试直接登录
t.Logf("register %s: %v (continue if already exists)", id, err)
}
}
func mustConnect(t *testing.T, ctx context.Context, ws, id, password string) *nixmsg.Client {
t.Helper()
c := nixmsg.New()
cred := nixmsg.Credential{Password: password}
if err := c.Connect(ctx, ws, id, cred, nixmsg.Options{ConnectTimeout: 20 * time.Second}); err != nil {
t.Fatalf("connect %s: %v", id, err)
}
return c
}
func waitMsg(t *testing.T, ch <-chan nixmsg.Message, d time.Duration) nixmsg.Message {
t.Helper()
select {
case m := <-ch:
return m
case <-time.After(d):
t.Fatal("timeout waiting message")
return nixmsg.Message{}
}
}
func waitConnState(t *testing.T, states <-chan nixmsg.ConnectionState, d time.Duration, want ...nixmsg.ConnectionState) nixmsg.ConnectionState {
t.Helper()
wants := map[nixmsg.ConnectionState]bool{}
for _, w := range want {
wants[w] = true
}
deadline := time.Now().Add(d)
for time.Now().Before(deadline) {
select {
case st := <-states:
if wants[st] {
return st
}
case <-time.After(50 * time.Millisecond):
}
}
t.Fatalf("timeout waiting state among %v", want)
return ""
}
func int64Ptr(v int64) *int64 { return &v }
func boolPtr(v bool) *bool { return &v }