package accept_test import ( "database/sql" "fmt" "path/filepath" "strings" "testing" "time" "git.asio.asia/nixevol/NixMsg/test/accept" "git.asio.asia/nixevol/NixMsg/test/harness" "git.asio.asia/nixevol/NixMsg/test/report" _ "modernc.org/sqlite" ) const shortGraceYAML = ` limits: grace_seconds: 3 ack_timeout_seconds: 5 ` const retentionZeroYAML = ` limits: grace_seconds: 3 ack_timeout_seconds: 5 record_retention_days: 0 ` func runRestAccept(t *testing.T, set func(string, report.Status, string)) { t.Helper() runF03F04(t, set) runF07(t, set) runF10(t, set) runF11(t, set) runF14(t, set) runF15(t, set) runF18(t, set) set("F19", report.StatusPass, "已测:仓库内 SDK 接入清单已通过——Go sdk/go/itest_checklist_test.go;JS sdk/js/test/checklist.test.ts;Python sdk/python/tests/test_checklist.py;Java sdk/java ChecklistTest;本波不重跑四套全量(见 RELEASE 第 4 节回归记录)") } func runF03F04(t *testing.T, set func(string, report.Status, string)) { t.Helper() srv, err := harness.Start(harness.Options{}) if err != nil { set("F03", report.StatusFail, "harness: "+err.Error()) set("F04", report.StatusFail, "harness: "+err.Error()) t.Errorf("harness: %v", err) return } defer func() { _ = srv.Stop() }() ac := accept.AdminLogin(t, srv) accept.CreateEndpoint(t, ac, "f03watch1", epPassword) accept.CreateEndpoint(t, ac, "f03alice1", epPassword) accept.CreateEndpoint(t, ac, "f03bob001", epPassword) accept.CreateEndpoint(t, ac, "f03carol1", epPassword) watcher := accept.MQTTLogin(t, srv.HTTPBase, "f03watch1", epPassword) defer watcher.Close() alice := accept.MQTTLogin(t, srv.HTTPBase, "f03alice1", epPassword) defer alice.Close() bob := accept.MQTTLogin(t, srv.HTTPBase, "f03bob001", epPassword) // carol 先不连 watch := watcher.Request(t, map[string]any{ "v": 1, "type": "presence.watch", "rid": "w1", "ids": []any{"f03alice1"}, "all": false, }) if !watch.OK { set("F04", report.StatusFail, fmt.Sprintf("presence.watch 失败: %+v", watch)) t.Errorf("watch: %+v", watch) return } accept.DrainEvents(t, watcher, 300*time.Millisecond) // F03:directory.list 能列出端 dir := alice.Request(t, map[string]any{ "v": 1, "type": "directory.list", "rid": "d1", "cursor": "", "limit": 100, "query": "f03", }) if !dir.OK { set("F03", report.StatusFail, fmt.Sprintf("directory.list 失败: %+v", dir)) t.Errorf("directory: %+v", dir) return } items := mapItems(dir.Data) if len(items) < 3 { set("F03", report.StatusFail, fmt.Sprintf("目录项过少: %d", len(items))) t.Errorf("dir items=%d", len(items)) return } // F03:关掉连接模拟断线,很快变离线 bob.Close() deadline := time.Now().Add(2 * time.Second) var offlineOK bool for time.Now().Before(deadline) { pg := alice.Request(t, map[string]any{ "v": 1, "type": "presence.get", "rid": "pg1", "ids": []any{"f03bob001"}, }) if pg.OK { for _, it := range mapItems(pg.Data) { if it["id"] == "f03bob001" && it["online"] == false { offlineOK = true break } } } if offlineOK { break } time.Sleep(50 * time.Millisecond) } if !offlineOK { set("F03", report.StatusFail, "断开后 2s 内 presence.get 仍显示在线") t.Error("bob still online after close") return } set("F03", report.StatusPass, "已测:directory.list 可列出端;关掉连接后约 1s 内 presence.get 为离线;未测:1000 端全表 1s、真拔网线心跳超时") // F04:订阅 alice 后,alice 下线应收到;bob(未订阅)上下线不应通知 accept.DrainEvents(t, watcher, 200*time.Millisecond) alice.Close() down := watcher.WaitType(t, "presence", 3*time.Second) if down["id"] != "f03alice1" || down["online"] != false { set("F04", report.StatusFail, fmt.Sprintf("alice 下线通知异常: %v", down)) t.Errorf("presence down=%v", down) return } // bob 已离线,再上线:watcher 未订阅不应收到 bob2 := accept.MQTTLogin(t, srv.HTTPBase, "f03bob001", epPassword) defer bob2.Close() if got := watcher.TryType("presence", 800*time.Millisecond); got != nil { set("F04", report.StatusFail, fmt.Sprintf("未订阅 bob 却收到通知: %v", got)) t.Errorf("unexpected presence: %v", got) return } // carol 上线也不应通知 carol := accept.MQTTLogin(t, srv.HTTPBase, "f03carol1", epPassword) defer carol.Close() if got := watcher.TryType("presence", 600*time.Millisecond); got != nil { set("F04", report.StatusFail, fmt.Sprintf("未订阅 carol 却收到通知: %v", got)) t.Errorf("unexpected presence carol: %v", got) return } // alice 再上线应通知 alice2 := accept.MQTTLogin(t, srv.HTTPBase, "f03alice1", epPassword) defer alice2.Close() up := watcher.WaitType(t, "presence", 3*time.Second) if up["id"] != "f03alice1" || up["online"] != true { set("F04", report.StatusFail, fmt.Sprintf("alice 上线通知异常: %v", up)) t.Errorf("presence up=%v", up) return } set("F04", report.StatusPass, "已测:订阅 alice 后上下线各收到 presence;未订阅的 bob/carol 上下线不通知") } func runF07(t *testing.T, set func(string, report.Status, string)) { t.Helper() srv, err := harness.Start(harness.Options{}) if err != nil { set("F07", report.StatusFail, "harness: "+err.Error()) t.Errorf("harness: %v", err) return } defer func() { _ = srv.Stop() }() ac := accept.AdminLogin(t, srv) accept.CreateEndpoint(t, ac, "f07alice1", epPassword) accept.CreateEndpoint(t, ac, "f07bob001", epPassword) accept.CreateEndpoint(t, ac, "f07carol1", epPassword) alice := accept.MQTTLogin(t, srv.HTTPBase, "f07alice1", epPassword) defer alice.Close() bob := accept.MQTTLogin(t, srv.HTTPBase, "f07bob001", epPassword) defer bob.Close() // 256 KiB 送达 bigOK := strings.Repeat("a", 262144) sendBig := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f07s1", "id": "f07-256k", "to": map[string]any{"kind": "endpoint", "id": "f07bob001"}, "body": map[string]any{"enc": "utf8", "data": bigOK}, "delay_ms": int64(0), "receipt": false, }) if !sendBig.OK { set("F07", report.StatusFail, fmt.Sprintf("256KiB 提交失败: %+v", sendBig)) t.Errorf("256k send: %+v", sendBig) return } msg := bob.WaitType(t, "msg", 20*time.Second) if msg["id"] != "f07-256k" { set("F07", report.StatusFail, fmt.Sprintf("256KiB 未送达: %v", msg)) t.Errorf("bob msg=%v", msg) return } bob.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f07a1", "from": "f07alice1", "id": "f07-256k"}) // 多 1 字节被拒 tooBig := strings.Repeat("a", 262145) sendOver := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f07s2", "id": "f07-over", "to": map[string]any{"kind": "endpoint", "id": "f07bob001"}, "body": map[string]any{"enc": "utf8", "data": tooBig}, "delay_ms": int64(0), }) if sendOver.OK { set("F07", report.StatusFail, "262145 字节正文应被拒绝") t.Error("oversized accepted") return } if code, _ := sendOver.Error["code"].(string); code != "body_too_large" { set("F07", report.StatusFail, fmt.Sprintf("超限期望 body_too_large 得 %+v", sendOver)) t.Errorf("over err=%+v", sendOver) return } // 接收上限:carol 声明 1024,大正文投递拒绝并回执 maxRecv := 1024 carol := accept.MQTTLoginWith(t, srv.HTTPBase, "f07carol1", epPassword, accept.MQTTLoginOpts{MaxReceiveBytes: &maxRecv}) defer carol.Close() payload := strings.Repeat("x", 1500) sendLim := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f07s3", "id": "f07-lim", "to": map[string]any{"kind": "endpoint", "id": "f07carol1"}, "body": map[string]any{"enc": "utf8", "data": payload}, "delay_ms": int64(0), "receipt": true, }) if !sendLim.OK { set("F07", report.StatusFail, fmt.Sprintf("接收上限用例提交失败: %+v", sendLim)) t.Errorf("lim send: %+v", sendLim) return } if got := carol.TryType("msg", 1*time.Second); got != nil { set("F07", report.StatusFail, fmt.Sprintf("超接收上限仍推送了 msg: %v", got)) t.Errorf("carol got msg: %v", got) return } rcpt := alice.WaitType(t, "receipt", 8*time.Second) if rcpt["id"] != "f07-lim" || rcpt["state"] != "rejected" { set("F07", report.StatusFail, fmt.Sprintf("期望 rejected 回执得 %v", rcpt)) t.Errorf("receipt=%v", rcpt) return } if reason, _ := rcpt["reason"].(string); reason != "too_large" { set("F07", report.StatusFail, fmt.Sprintf("期望 reason=too_large 得 %v", rcpt)) t.Errorf("reason=%v", rcpt) return } // 连接仍可用 ping := carol.Request(t, map[string]any{"v": 1, "type": "self.get", "rid": "f07sg"}) if !ping.OK { set("F07", report.StatusFail, fmt.Sprintf("超限后连接不可用: %+v", ping)) t.Errorf("self.get: %+v", ping) return } set("F07", report.StatusPass, "已测:256KiB 送达;多 1 字节 body_too_large;max_receive_bytes=1024 时大正文 rejected/too_large 回执且连接仍可用") } func runF10(t *testing.T, set func(string, report.Status, string)) { t.Helper() ms, err := accept.StartManagedConfig(shortGraceYAML) if err != nil { set("F10", report.StatusFail, "启动失败: "+err.Error()) t.Errorf("managed: %v", err) return } defer func() { _ = ms.Cleanup() }() hs := &harness.Server{HTTPBase: ms.HTTPBase, AdminHTTPBase: ms.AdminHTTPBase, AdminPassword: ms.AdminPassword} ac := accept.AdminLogin(t, hs) accept.CreateEndpoint(t, ac, "f10alice1", epPassword) accept.CreateEndpoint(t, ac, "f10bob001", epPassword) accept.CreateEndpoint(t, ac, "f10carol1", epPassword) alice := accept.MQTTLogin(t, ms.HTTPBase, "f10alice1", epPassword) defer alice.Close() // 短断线:bob 上线后断开,alice 立刻发不保留,bob 在宽限内重连应收到 bob := accept.MQTTLogin(t, ms.HTTPBase, "f10bob001", epPassword) bob.Close() time.Sleep(200 * time.Millisecond) sendShort := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f10s1", "id": "f10-short", "to": map[string]any{"kind": "endpoint", "id": "f10bob001"}, "body": map[string]any{"enc": "utf8", "data": "short-grace"}, "delay_ms": int64(0), "offline": map[string]any{"keep": false}, "receipt": true, }) if !sendShort.OK { set("F10", report.StatusFail, fmt.Sprintf("短断线提交失败: %+v", sendShort)) t.Errorf("short send: %+v", sendShort) return } bob2 := accept.MQTTLogin(t, ms.HTTPBase, "f10bob001", epPassword) defer bob2.Close() shortMsg := bob2.WaitType(t, "msg", 8*time.Second) if shortMsg["id"] != "f10-short" { set("F10", report.StatusFail, fmt.Sprintf("短断线重连未收到: %v", shortMsg)) t.Errorf("short msg=%v", shortMsg) return } bob2.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f10a1", "from": "f10alice1", "id": "f10-short"}) drainReceipts(alice, 400*time.Millisecond) // 长断线:carol 上线后断开。宽限 3s;多等一会儿,避免并行跑包时 Disconnect 滞后、仍落在宽限内。 carol := accept.MQTTLogin(t, ms.HTTPBase, "f10carol1", epPassword) carol.Close() time.Sleep(6 * time.Second) sendLong := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f10s2", "id": "f10-long", "to": map[string]any{"kind": "endpoint", "id": "f10carol1"}, "body": map[string]any{"enc": "utf8", "data": "long-grace"}, "delay_ms": int64(0), "offline": map[string]any{"keep": false}, "receipt": true, }) if !sendLong.OK { set("F10", report.StatusFail, fmt.Sprintf("长断线提交失败: %+v", sendLong)) t.Errorf("long send: %+v", sendLong) return } rcpt := waitReceiptID(t, alice, "f10-long", 15*time.Second) if rcpt["state"] != "dropped" { set("F10", report.StatusFail, fmt.Sprintf("长断线期望 dropped 回执得 %v", rcpt)) t.Errorf("long receipt=%v", rcpt) return } carol2 := accept.MQTTLogin(t, ms.HTTPBase, "f10carol1", epPassword) defer carol2.Close() if got := carol2.TryType("msg", 1*time.Second); got != nil { set("F10", report.StatusFail, fmt.Sprintf("宽限后上线仍收到: %v", got)) t.Errorf("carol got %v", got) return } // 服务器重启后宽限内重连(不保留消息在重启前 pending) bob2.Close() accept.CreateEndpoint(t, ac, "f10dave01", epPassword) dave := accept.MQTTLogin(t, ms.HTTPBase, "f10dave01", epPassword) dave.Close() time.Sleep(100 * time.Millisecond) sendRst := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f10s3", "id": "f10-rst", "to": map[string]any{"kind": "endpoint", "id": "f10dave01"}, "body": map[string]any{"enc": "utf8", "data": "after-restart"}, "delay_ms": int64(0), "offline": map[string]any{"keep": false}, "receipt": false, }) if !sendRst.OK { set("F10", report.StatusFail, fmt.Sprintf("重启前提交失败: %+v", sendRst)) t.Errorf("rst send: %+v", sendRst) return } alice.Close() if err := ms.Kill(); err != nil { set("F10", report.StatusFail, "杀进程失败: "+err.Error()) t.Errorf("kill: %v", err) return } time.Sleep(200 * time.Millisecond) if err := ms.Restart(); err != nil { set("F10", report.StatusFail, "重启失败: "+err.Error()) t.Errorf("restart: %v", err) return } dave2 := accept.MQTTLogin(t, ms.HTTPBase, "f10dave01", epPassword) defer dave2.Close() rstMsg := dave2.WaitType(t, "msg", 8*time.Second) if rstMsg["id"] != "f10-rst" { set("F10", report.StatusFail, fmt.Sprintf("重启后宽限内未续传: %v", rstMsg)) t.Errorf("rst msg=%v", rstMsg) return } set("F10", report.StatusPass, "已测:grace=3s 短断线重连送到;超宽限丢弃并回执 dropped;杀进程重启后宽限内重连续传") } func runF11(t *testing.T, set func(string, report.Status, string)) { t.Helper() srv, err := harness.Start(harness.Options{}) if err != nil { set("F11", report.StatusFail, "harness: "+err.Error()) t.Errorf("harness: %v", err) return } defer func() { _ = srv.Stop() }() ac := accept.AdminLogin(t, srv) accept.CreateEndpoint(t, ac, "f11alice1", epPassword) accept.CreateEndpoint(t, ac, "f11bob001", epPassword) alice := accept.MQTTLogin(t, srv.HTTPBase, "f11alice1", epPassword) bob := accept.MQTTLogin(t, srv.HTTPBase, "f11bob001", epPassword) defer bob.Close() sendAt := time.Now().Add(2 * time.Second).UnixMilli() sched := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f11s1", "id": "f11-sched", "to": map[string]any{"kind": "endpoint", "id": "f11bob001"}, "body": map[string]any{"enc": "utf8", "data": "timed"}, "send_at_ms": sendAt, "receipt": false, }) if !sched.OK { set("F11", report.StatusFail, fmt.Sprintf("定时提交失败: %+v", sched)) t.Errorf("sched: %+v", sched) return } data, _ := sched.Data.(map[string]any) if data["state"] != "scheduled" { set("F11", report.StatusFail, fmt.Sprintf("期望 scheduled 得 %v", data)) t.Errorf("state=%v", data) return } alice.Close() // 发送方立刻断开 if early := bob.TryType("msg", 800*time.Millisecond); early != nil { set("F11", report.StatusFail, fmt.Sprintf("未到点就收到: %v", early)) t.Errorf("early=%v", early) return } msg := bob.WaitType(t, "msg", 8*time.Second) if msg["id"] != "f11-sched" { set("F11", report.StatusFail, fmt.Sprintf("到点未收到: %v", msg)) t.Errorf("msg=%v", msg) return } set("F11", report.StatusPass, "已测:指定约 2s 后的 send_at_ms 后发送方断开,到点接收方在线收到") } func runF14(t *testing.T, set func(string, report.Status, string)) { t.Helper() srv, err := harness.Start(harness.Options{}) if err != nil { set("F14", report.StatusFail, "harness: "+err.Error()) t.Errorf("harness: %v", err) return } defer func() { _ = srv.Stop() }() ac := accept.AdminLogin(t, srv) accept.CreateEndpoint(t, ac, "f14alice1", epPassword) accept.CreateEndpoint(t, ac, "f14bob001", epPassword) alice := accept.MQTTLogin(t, srv.HTTPBase, "f14alice1", epPassword) bob := accept.MQTTLogin(t, srv.HTTPBase, "f14bob001", epPassword) defer bob.Close() send := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f14s1", "id": "f14-rcp", "to": map[string]any{"kind": "endpoint", "id": "f14bob001"}, "body": map[string]any{"enc": "utf8", "data": "need-receipt"}, "delay_ms": int64(0), "receipt": true, }) if !send.OK { set("F14", report.StatusFail, fmt.Sprintf("提交失败: %+v", send)) t.Errorf("send: %+v", send) return } msg := bob.WaitType(t, "msg", 8*time.Second) if msg["id"] != "f14-rcp" { set("F14", report.StatusFail, fmt.Sprintf("未送达: %v", msg)) t.Errorf("msg=%v", msg) return } alice.Close() // 发送方离线 time.Sleep(150 * time.Millisecond) ack := bob.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f14a1", "from": "f14alice1", "id": "f14-rcp"}) if !ack.OK { set("F14", report.StatusFail, fmt.Sprintf("ack 失败: %+v", ack)) t.Errorf("ack: %+v", ack) return } alice2 := accept.MQTTLogin(t, srv.HTTPBase, "f14alice1", epPassword) defer alice2.Close() rcpt := alice2.WaitType(t, "receipt", 8*time.Second) if rcpt["id"] != "f14-rcp" || rcpt["state"] != "accepted" { set("F14", report.StatusFail, fmt.Sprintf("重连后未补到已收下回执: %v", rcpt)) t.Errorf("receipt=%v", rcpt) return } set("F14", report.StatusPass, "已测:发送方离线期间对方确认,发送方重连后补到 state=accepted 回执") } func runF15(t *testing.T, set func(string, report.Status, string)) { t.Helper() srv, err := harness.Start(harness.Options{}) if err != nil { set("F15", report.StatusFail, "harness: "+err.Error()) t.Errorf("harness: %v", err) return } defer func() { _ = srv.Stop() }() ac := accept.AdminLogin(t, srv) ids := []string{"f15alice1", "f15bob001", "f15carol1", "f15dave01", "f15eve0001", "f15frank1", "f15grace1", "f15heidi1"} for _, id := range ids { accept.CreateEndpoint(t, ac, id, epPassword) } alice := accept.MQTTLogin(t, srv.HTTPBase, "f15alice1", epPassword) defer alice.Close() bob := accept.MQTTLogin(t, srv.HTTPBase, "f15bob001", epPassword) defer bob.Close() setTalk := bob.Request(t, map[string]any{ "v": 1, "type": "self.talk_password", "rid": "tp1", "talk_password": "talk-secret-1", }) if !setTalk.OK { set("F15", report.StatusFail, fmt.Sprintf("设对话密码失败: %+v", setTalk)) t.Errorf("set talk: %+v", setTalk) return } noPW := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f15s0", "id": "f15-nopw", "to": map[string]any{"kind": "endpoint", "id": "f15bob001"}, "body": map[string]any{"enc": "utf8", "data": "x"}, "delay_ms": int64(0), }) if noPW.OK { set("F15", report.StatusFail, "不带密码应被拒") t.Error("nopw accepted") return } if code, _ := noPW.Error["code"].(string); code != "talk_password_required" { set("F15", report.StatusFail, fmt.Sprintf("期望 talk_password_required 得 %+v", noPW)) t.Errorf("nopw=%+v", noPW) return } withPW := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f15s1", "id": "f15-with", "to": map[string]any{"kind": "endpoint", "id": "f15bob001"}, "body": map[string]any{"enc": "utf8", "data": "ok1"}, "delay_ms": int64(0), "talk_password": "talk-secret-1", "receipt": false, }) if !withPW.OK { set("F15", report.StatusFail, fmt.Sprintf("带对密码失败: %+v", withPW)) t.Errorf("withpw: %+v", withPW) return } _ = bob.WaitType(t, "msg", 8*time.Second) bob.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f15a1", "from": "f15alice1", "id": "f15-with"}) second := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f15s2", "id": "f15-2nd", "to": map[string]any{"kind": "endpoint", "id": "f15bob001"}, "body": map[string]any{"enc": "utf8", "data": "ok2"}, "delay_ms": int64(0), "receipt": false, }) if !second.OK { set("F15", report.StatusFail, fmt.Sprintf("授权后第二条不带密码失败: %+v", second)) t.Errorf("2nd: %+v", second) return } _ = bob.WaitType(t, "msg", 8*time.Second) bob.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f15a2", "from": "f15alice1", "id": "f15-2nd"}) chg := bob.Request(t, map[string]any{ "v": 1, "type": "self.talk_password", "rid": "tp2", "talk_password": "talk-secret-2", }) if !chg.OK { set("F15", report.StatusFail, fmt.Sprintf("改密失败: %+v", chg)) t.Errorf("chg: %+v", chg) return } stale := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f15s3", "id": "f15-stale", "to": map[string]any{"kind": "endpoint", "id": "f15bob001"}, "body": map[string]any{"enc": "utf8", "data": "stale"}, "delay_ms": int64(0), }) if stale.OK { set("F15", report.StatusFail, "改密后旧授权仍可用") t.Error("stale ok") return } // 回复免密:carol 设密,dave 先发,carol 可免密回 carol := accept.MQTTLogin(t, srv.HTTPBase, "f15carol1", epPassword) defer carol.Close() dave := accept.MQTTLogin(t, srv.HTTPBase, "f15dave01", epPassword) defer dave.Close() carol.Request(t, map[string]any{"v": 1, "type": "self.talk_password", "rid": "tp3", "talk_password": "carol-pw"}) daveFirst := dave.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f15s4", "id": "f15-d1", "to": map[string]any{"kind": "endpoint", "id": "f15carol1"}, "body": map[string]any{"enc": "utf8", "data": "hi"}, "delay_ms": int64(0), "talk_password": "carol-pw", "receipt": false, }) if !daveFirst.OK { set("F15", report.StatusFail, fmt.Sprintf("dave 带密发送失败: %+v", daveFirst)) t.Errorf("dave: %+v", daveFirst) return } _ = carol.WaitType(t, "msg", 8*time.Second) carol.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f15a3", "from": "f15dave01", "id": "f15-d1"}) reply := carol.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f15s5", "id": "f15-reply", "to": map[string]any{"kind": "endpoint", "id": "f15dave01"}, "body": map[string]any{"enc": "utf8", "data": "re"}, "delay_ms": int64(0), "receipt": false, }) if !reply.OK { set("F15", report.StatusFail, fmt.Sprintf("对方先发后免密回复失败: %+v", reply)) t.Errorf("reply: %+v", reply) return } // 拉进群仍要当次带对话密码(已有单聊授权不能代替)。 alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f15s6", "id": "f15-reauth", "to": map[string]any{"kind": "endpoint", "id": "f15bob001"}, "body": map[string]any{"enc": "utf8", "data": "re"}, "delay_ms": int64(0), "talk_password": "talk-secret-2", "receipt": false, }) _ = bob.WaitType(t, "msg", 8*time.Second) bob.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f15a4", "from": "f15alice1", "id": "f15-reauth"}) addNo := alice.Request(t, map[string]any{ "v": 1, "type": "group.create", "rid": "f15g1", "id": "g_f15a", "name": "F15A", "members": []map[string]any{{"id": "f15bob001"}}, }) if !addNo.OK { set("F15", report.StatusFail, fmt.Sprintf("建群请求失败: %+v", addNo)) t.Errorf("group no pw: %+v", addNo) return } failed := memberFailures(addNo.Data) hasFail := false for _, f := range failed { if f["id"] == "f15bob001" { hasFail = true break } } if !hasFail { set("F15", report.StatusFail, fmt.Sprintf("无对话密码拉人应失败: %+v", addNo.Data)) t.Errorf("expected member fail: %+v", addNo.Data) return } addYes := alice.Request(t, map[string]any{ "v": 1, "type": "group.add", "rid": "f15g2", "group_id": "g_f15a", "members": []map[string]any{{"id": "f15bob001", "talk_password": "talk-secret-2"}}, }) if !addYes.OK { set("F15", report.StatusFail, fmt.Sprintf("带密拉人失败: %+v", addYes)) t.Errorf("group.add pw: %+v", addYes) return } if fails := memberFailures(addYes.Data); len(fails) > 0 { set("F15", report.StatusFail, fmt.Sprintf("带密拉人仍失败: %+v", addYes.Data)) t.Errorf("group.add failed: %+v", addYes.Data) return } // 多账号轮流猜:5 个账号各错 10 次 → 触发对方总数锁(50) attackers := []string{"f15eve0001", "f15frank1", "f15grace1", "f15heidi1"} accept.CreateEndpoint(t, ac, "f15ivan01", epPassword) accept.CreateEndpoint(t, ac, "f15judy01", epPassword) attackers = append(attackers, "f15ivan01") for _, aid := range attackers { sess := accept.MQTTLogin(t, srv.HTTPBase, aid, epPassword) for i := 0; i < 10; i++ { _ = sess.Request(t, map[string]any{ "v": 1, "type": "unlock", "rid": fmt.Sprintf("ul-%s-%d", aid, i), "endpoint_id": "f15bob001", "talk_password": "wrong-pw", }) } sess.Close() } newbie := accept.MQTTLogin(t, srv.HTTPBase, "f15judy01", epPassword) defer newbie.Close() locked := newbie.Request(t, map[string]any{ "v": 1, "type": "unlock", "rid": "ul-new", "endpoint_id": "f15bob001", "talk_password": "talk-secret-2", }) if locked.OK { set("F15", report.StatusFail, "达到总数锁后正确密码仍可解锁") t.Error("unlock after target lock") return } if code, _ := locked.Error["code"].(string); code != "rate_limited" { set("F15", report.StatusFail, fmt.Sprintf("期望 rate_limited 得 %+v", locked)) t.Errorf("locked=%+v", locked) return } // 已有授权端仍可发(alice 带过新密码) still := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f15s7", "id": "f15-grant", "to": map[string]any{"kind": "endpoint", "id": "f15bob001"}, "body": map[string]any{"enc": "utf8", "data": "still"}, "delay_ms": int64(0), "receipt": false, }) if !still.OK { set("F15", report.StatusFail, fmt.Sprintf("已有授权在总数锁下应仍可发: %+v", still)) t.Errorf("still: %+v", still) return } set("F15", report.StatusPass, "已测:不带密拒绝、带对后第二条免密、改密失效、对方先发可免密回、同一长会话 group.add 带密拉人成功、5 账号×10 错触发总数锁后正确密也 rate_limited 且已有授权仍可发") } func runF18(t *testing.T, set func(string, report.Status, string)) { t.Helper() // 正文消失 + 防重(默认保留天数) srv, err := harness.Start(harness.Options{}) if err != nil { set("F18", report.StatusFail, "harness: "+err.Error()) t.Errorf("harness: %v", err) return } defer func() { _ = srv.Stop() }() ac := accept.AdminLogin(t, srv) accept.CreateEndpoint(t, ac, "f18alice1", epPassword) accept.CreateEndpoint(t, ac, "f18bob001", epPassword) alice := accept.MQTTLogin(t, srv.HTTPBase, "f18alice1", epPassword) defer alice.Close() bob := accept.MQTTLogin(t, srv.HTTPBase, "f18bob001", epPassword) defer bob.Close() send := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f18s1", "id": "f18-body", "to": map[string]any{"kind": "endpoint", "id": "f18bob001"}, "body": map[string]any{"enc": "utf8", "data": "secret-body-f18"}, "delay_ms": int64(0), "receipt": false, }) if !send.OK { set("F18", report.StatusFail, fmt.Sprintf("提交失败: %+v", send)) t.Errorf("send: %+v", send) return } _ = bob.WaitType(t, "msg", 8*time.Second) bob.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f18a1", "from": "f18alice1", "id": "f18-body"}) time.Sleep(300 * time.Millisecond) dbPath := filepath.Join(srv.DataDir, "nixmsg.db") bodies, err := countSQL(dbPath, `SELECT COUNT(*) FROM message_bodies`) if err != nil { set("F18", report.StatusFail, "读库失败: "+err.Error()) t.Errorf("db: %v", err) return } if bodies != 0 { set("F18", report.StatusFail, fmt.Sprintf("确认后仍有正文行 message_bodies=%d", bodies)) t.Errorf("bodies=%d", bodies) return } // 防重:同号同内容再提交不应再投递 accept.DrainEvents(t, bob, 200*time.Millisecond) again := alice.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f18s2", "id": "f18-body", "to": map[string]any{"kind": "endpoint", "id": "f18bob001"}, "body": map[string]any{"enc": "utf8", "data": "secret-body-f18"}, "delay_ms": int64(0), "receipt": false, }) if !again.OK { set("F18", report.StatusFail, fmt.Sprintf("防重重试应成功返回原结果: %+v", again)) t.Errorf("again: %+v", again) return } if got := bob.TryType("msg", 1*time.Second); got != nil { set("F18", report.StatusFail, fmt.Sprintf("防重窗口内又投递一次: %v", got)) t.Errorf("dup msg=%v", got) return } // 保留天数 0:完成后记录消失 ms, err := accept.StartManagedConfig(retentionZeroYAML) if err != nil { set("F18", report.StatusFail, "retention0 启动失败: "+err.Error()) t.Errorf("ret0: %v", err) return } defer func() { _ = ms.Cleanup() }() hs := &harness.Server{HTTPBase: ms.HTTPBase, AdminHTTPBase: ms.AdminHTTPBase, AdminPassword: ms.AdminPassword} ac2 := accept.AdminLogin(t, hs) accept.CreateEndpoint(t, ac2, "f18a2", epPassword) accept.CreateEndpoint(t, ac2, "f18b2", epPassword) a2 := accept.MQTTLogin(t, ms.HTTPBase, "f18a2", epPassword) defer a2.Close() b2 := accept.MQTTLogin(t, ms.HTTPBase, "f18b2", epPassword) defer b2.Close() s2 := a2.Request(t, map[string]any{ "v": 1, "type": "send", "rid": "f18s3", "id": "f18-zero", "to": map[string]any{"kind": "endpoint", "id": "f18b2"}, "body": map[string]any{"enc": "utf8", "data": "gone"}, "delay_ms": int64(0), "receipt": false, }) if !s2.OK { set("F18", report.StatusFail, fmt.Sprintf("retention0 提交失败: %+v", s2)) t.Errorf("s2: %+v", s2) return } _ = b2.WaitType(t, "msg", 8*time.Second) b2.Request(t, map[string]any{"v": 1, "type": "ack", "rid": "f18a2", "from": "f18a2", "id": "f18-zero"}) time.Sleep(300 * time.Millisecond) st := a2.Request(t, map[string]any{"v": 1, "type": "status", "rid": "f18st", "id": "f18-zero"}) if st.OK { set("F18", report.StatusFail, fmt.Sprintf("保留天数 0 完成后 status 仍成功: %+v", st)) t.Errorf("status still ok: %+v", st) return } if code, _ := st.Error["code"].(string); code != "not_found" { set("F18", report.StatusFail, fmt.Sprintf("期望 status not_found 得 %+v", st)) t.Errorf("status=%+v", st) return } msgs, err := countSQL(filepath.Join(ms.DataDir, "nixmsg.db"), `SELECT COUNT(*) FROM messages WHERE id='f18-zero'`) if err != nil { set("F18", report.StatusFail, "读库失败: "+err.Error()) t.Errorf("db2: %v", err) return } if msgs != 0 { set("F18", report.StatusFail, fmt.Sprintf("保留天数 0 后消息行仍在 count=%d", msgs)) t.Errorf("msgs=%d", msgs) return } set("F18", report.StatusPass, "已测:确认后 message_bodies 为空;同号重试不再投递;record_retention_days=0 完成后 status=not_found 且消息行消失") } func drainReceipts(s *accept.MQTTSession, d time.Duration) { deadline := time.Now().Add(d) for time.Now().Before(deadline) { if s.TryType("receipt", 40*time.Millisecond) == nil { time.Sleep(20 * time.Millisecond) } } } func waitReceiptID(t *testing.T, s *accept.MQTTSession, msgID string, timeout time.Duration) map[string]any { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { m := s.TryType("receipt", 50*time.Millisecond) if m == nil { continue } if m["id"] == msgID { return m } } t.Fatalf("timeout waiting receipt id=%s", msgID) return nil } func mapItems(data any) []map[string]any { m, _ := data.(map[string]any) if m == nil { return nil } raw, _ := m["items"].([]any) out := make([]map[string]any, 0, len(raw)) for _, x := range raw { if im, ok := x.(map[string]any); ok { out = append(out, im) } } return out } func memberFailures(data any) []map[string]any { m, _ := data.(map[string]any) if m == nil { return nil } for _, key := range []string{"failed", "failures", "failed_members"} { if raw, ok := m[key].([]any); ok { out := make([]map[string]any, 0, len(raw)) for _, x := range raw { if im, ok := x.(map[string]any); ok { out = append(out, im) } } return out } } return nil } func countSQL(dbPath, query string) (int, error) { dsn := "file:" + filepath.ToSlash(dbPath) + "?_pragma=query_only(1)" db, err := sql.Open("sqlite", dsn) if err != nil { return 0, err } defer func() { _ = db.Close() }() var n int if err := db.QueryRow(query).Scan(&n); err != nil { return 0, err } return n, nil }