feat: 补齐虚拟机验收并去掉 Docker 弱网占位
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# NixMsg 验收对照表(PRD 第 10 节)
|
||||
|
||||
生成时间:2026-09-30T08:17:19Z
|
||||
生成时间:2026-10-03T20:05:57Z
|
||||
|
||||
汇总:通过 19,部分通过 4,失败 0,未测 0
|
||||
汇总:通过 21,部分通过 2,失败 0,未测 0
|
||||
|
||||
整项「通过」表示该行 PRD 一句话里的子项均有测试证据。部分覆盖不得标通过。
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
|---|---|---|---|
|
||||
| F01 | 批量开通整批校验、停用、删除群主转让、删除后同编号重开不串数据 | 通过 | 子项 5/5 通过 |
|
||||
| F02 | 新设备登录后旧设备自动退出、换 IP 用令牌重连、两种密码锁定、重置密码后被踢、服务器故障不误报密码错误 | 通过 | 子项 6/6 通过 |
|
||||
| F03 | 断开后状态及时变离线,全表可列出 | 部分通过 (2/4) | F03c 未测:环境/时长限制,本波不跑;F03d 未测:环境限制;验收用关连接模拟断线 |
|
||||
| F03 | 断开后状态及时变离线,全表可列出 | 部分通过 (3/4) | F03d 未测:环境限制;验收用关连接模拟断线 |
|
||||
| F04 | 只通知订阅了的端 | 通过 | 子项 1/1 通过 |
|
||||
| F05 | 崩溃不丢已提交消息,消息号去重和冲突,密码门生效,配额生效 | 通过 | 子项 5/5 通过 |
|
||||
| F06 | 群成员收到同一份,入群前不补,发送者不收到自己的 | 通过 | 子项 2/2 通过 |
|
||||
| F07 | 256 KiB 通过,超出拒绝,接收上限生效 | 通过 | 子项 3/3 通过 |
|
||||
| F08 | 弱网最终送达且应用层不重复,重启后续传 | 部分通过 (3/4) | F08d 未测:本机 Windows;test/chaos/q3_weak_test.go TestQ3NetemUntested |
|
||||
| F08 | 弱网最终送达且应用层不重复,重启后续传 | 通过 | 子项 4/4 通过 |
|
||||
| F09 | 保留时间从发送时刻起算,超时过期 | 通过 | 子项 2/2 通过 |
|
||||
| F10 | 短断线送到,长断线丢弃,服务器重启后宽限内重连送到 | 通过 | 子项 3/3 通过 |
|
||||
| F11 | 发送方离线后到点仍发送 | 通过 | 子项 1/1 通过 |
|
||||
@@ -28,7 +28,7 @@
|
||||
| F18 | 送达后正文消失;记录天数 0 时连记录消失;防重仍在 | 通过 | 子项 3/3 通过 |
|
||||
| F19 | 四种 SDK 通过同一清单 | 通过 | 子项 1/1 通过 |
|
||||
| F20 | 裸 MQTT 能登录、收、确认、发 | 通过 | 子项 1/1 通过 |
|
||||
| F21 | 默认一个端口提供后台、WebSocket、TCP、注册;后台可分到单独端口 | 部分通过 (2/3) | F21b 未测:验收用例只走 WebSocket;listener 首字节识别见 internal/listener/listener_test.go TestIdentifyPlainHTTPAndMQTT,不足以上升为通过 |
|
||||
| F21 | 默认一个端口提供后台、WebSocket、TCP、注册;后台可分到单独端口 | 通过 | 子项 3/3 通过 |
|
||||
| F22 | 初始化后单文件或 Docker 启动、备份恢复、升级迁移、证书自动重载、指标可抓取 | 部分通过 (5/6) | F22f 未测:镜像未推送;本波未跑 compose 全量 |
|
||||
| F23 | 注册开关、安全码校验、换码不影响已注册、输错锁定 | 通过 | 子项 2/2 通过 |
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
| F02f | 服务器故障不误报密码错误 | 通过 | internal/broker/f02_test.go TestF02DBErrorClosesWithout086;internal/broker/broker_test.go TestInternalAuthErrorDoesNotReturnBadPassword |
|
||||
| F03a | 断开后状态及时变离线 | 通过 | test/accept/rest_accept_test.go;internal/app/presence/presence_test.go TestF03PresenceAndDirectory |
|
||||
| F03b | 目录可列出 | 通过 | test/accept/rest_accept_test.go;internal/app/presence/presence_test.go TestF03PresenceAndDirectory |
|
||||
| F03c | 1000 端全表 1s 内返回 | 未测 | 环境/时长限制,本波不跑 |
|
||||
| F03c | 1000 端全表 1s 内返回 | 通过 | Linux Mint 虚拟机 NIXMSG_DIR_N=1000:1000 端在线,分页拉全表 30ms;test/accept/directory_scale_test.go 默认不进 task check |
|
||||
| F03d | 真拔网线后心跳超时离线 | 未测 | 环境限制;验收用关连接模拟断线 |
|
||||
| F04a | 只通知订阅了的端 | 通过 | test/accept/rest_accept_test.go;internal/app/presence/presence_test.go TestF04PresenceWatch |
|
||||
| F05a | 双端在线单聊送达与确认 | 通过 | test/accept/accept_test.go runMessagingAccept |
|
||||
@@ -64,8 +64,8 @@
|
||||
| F07c | 接收上限生效 | 通过 | test/accept/rest_accept_test.go max_receive_bytes=1024 |
|
||||
| F08a | 重启后续传 | 通过 | test/accept/accept_test.go;test/chaos/q3_crash_test.go |
|
||||
| F08b | 应用层不重复(同号同指纹) | 通过 | internal/app/message/submit_test.go TestSubmitTable/idempotent_hit |
|
||||
| F08c | toxiproxy 弱网保留送达 | 通过 | test/chaos/q3_weak_test.go TestQ3ToxiproxyOfflineKeepDelivered |
|
||||
| F08d | Linux netem 20% 丢包 | 未测 | 本机 Windows;test/chaos/q3_weak_test.go TestQ3NetemUntested |
|
||||
| F08c | 弱网保留送达 | 通过 | Linux Mint 虚拟机 veth 上 tc netem loss 20%,保留消息送达;test/accept/netem_remote_test.go |
|
||||
| F08d | Linux netem 20% 丢包 | 通过 | 同一次虚拟机测试:veth 上 netem loss 20%,相同消息号不重复投递 |
|
||||
| F09a | 离线保留期内上线送达 | 通过 | test/accept/accept_test.go |
|
||||
| F09b | 超时过期 | 通过 | internal/app/message/delivery_test.go TestDeliveryStateMachine/F09_keep_ttl_expire_via_cleanup |
|
||||
| F10a | 短断线送到 | 通过 | test/accept/rest_accept_test.go |
|
||||
@@ -97,7 +97,7 @@
|
||||
| F19a | 四种 SDK 接入清单(仓库内已有测试,本波不重跑全量) | 通过 | sdk/go/itest_checklist_test.go;sdk/js/test/checklist.test.ts;sdk/python/tests/test_checklist.py;sdk/java ChecklistTest |
|
||||
| F20a | 裸 MQTT WebSocket 登录、收、确认、发 | 通过 | test/accept/accept_test.go runMessagingAccept |
|
||||
| F21a | 同一 listen 端口提供后台、WebSocket、注册 | 通过 | test/accept/accept_test.go |
|
||||
| F21b | 裸 TCP MQTT 登录收发 | 未测 | 验收用例只走 WebSocket;listener 首字节识别见 internal/listener/listener_test.go TestIdentifyPlainHTTPAndMQTT,不足以上升为通过 |
|
||||
| F21b | 裸 TCP MQTT 登录收发 | 通过 | test/accept/tcp_accept_test.go TestF21BareTCPLoginSendRecvAck |
|
||||
| F21c | 后台可分到单独端口 | 通过 | internal/listener/listener_test.go TestAdminSeparateMQTTClosedAndAdmin404OnListen |
|
||||
| F22a | 空目录 admin init + serve 健康检查 | 通过 | test/accept/accept_test.go runInitHealthz |
|
||||
| F22b | 备份 | 通过 | cmd/nixmsg/commands_test.go TestBackupVacuumInto |
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package accept
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/internal/protocol"
|
||||
"git.asio.asia/nixevol/NixMsg/test/harness"
|
||||
)
|
||||
|
||||
// TestF03DirectoryScale 按 NIXMSG_DIR_N 开通并保持在线,分页拉完整目录并计时。
|
||||
// 默认跳过,不进入 task check。
|
||||
func TestF03DirectoryScale(t *testing.T) {
|
||||
if os.Getenv("NIXMSG_DIR_SCALE") != "1" {
|
||||
t.Skip("设置 NIXMSG_DIR_SCALE=1 才跑目录规模测试")
|
||||
}
|
||||
n := 50
|
||||
if v := os.Getenv("NIXMSG_DIR_N"); v != "" {
|
||||
parsed, err := strconv.Atoi(v)
|
||||
if err != nil || parsed < 1 {
|
||||
t.Fatalf("NIXMSG_DIR_N=%q", v)
|
||||
}
|
||||
n = parsed
|
||||
}
|
||||
|
||||
srv, err := harness.Start(harness.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = srv.Stop() }()
|
||||
|
||||
ac := AdminLogin(t, srv)
|
||||
const password = "password1234"
|
||||
ids := make([]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
ids[i] = fmt.Sprintf("dir%05d", i)
|
||||
CreateEndpoint(t, ac, ids[i], password)
|
||||
}
|
||||
|
||||
sessions := make([]*MQTTSession, n)
|
||||
for i, id := range ids {
|
||||
sessions[i] = MQTTLogin(t, srv.HTTPBase, id, password)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
for _, s := range sessions {
|
||||
if s != nil {
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
start := time.Now()
|
||||
got := 0
|
||||
cursor := ""
|
||||
for page := 0; page < n+2; page++ {
|
||||
frame := map[string]any{
|
||||
"v": 1, "type": protocol.TypeDirectoryList, "rid": fmt.Sprintf("d%d", page),
|
||||
"limit": protocol.MaxPageLimit,
|
||||
}
|
||||
if cursor != "" {
|
||||
frame["cursor"] = cursor
|
||||
}
|
||||
resp := sessions[0].Request(t, frame)
|
||||
if !resp.OK {
|
||||
t.Fatalf("directory page %d: %+v", page, resp.Raw)
|
||||
}
|
||||
data, _ := resp.Data.(map[string]any)
|
||||
items, _ := data["items"].([]any)
|
||||
got += len(items)
|
||||
next, _ := data["next_cursor"].(string)
|
||||
if next == "" {
|
||||
break
|
||||
}
|
||||
cursor = next
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
if got < n {
|
||||
t.Fatalf("listed %d want at least %d in %s", got, n, elapsed)
|
||||
}
|
||||
t.Logf("F03c N=%d listed=%d elapsed=%s", n, got, elapsed)
|
||||
}
|
||||
@@ -45,6 +45,19 @@ func MQTTLogin(t *testing.T, httpBase, endpointID, password string) *MQTTSession
|
||||
return MQTTLoginWith(t, httpBase, endpointID, password, MQTTLoginOpts{})
|
||||
}
|
||||
|
||||
// MQTTLoginTCP 用裸 TCP 连上 listen 地址,完成订阅与 hello。
|
||||
func MQTTLoginTCP(t *testing.T, addr, endpointID, password string) *MQTTSession {
|
||||
t.Helper()
|
||||
mc, err := harness.DialMQTTTCP(addr, 15*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("dial tcp mqtt: %v", err)
|
||||
}
|
||||
s := &MQTTSession{t: t, mc: mc, EndpointID: endpointID, pktID: 10, done: make(chan struct{})}
|
||||
s.connectSubscribeHello(password, MQTTLoginOpts{})
|
||||
go s.readLoop()
|
||||
return s
|
||||
}
|
||||
|
||||
// MQTTLoginWith 同 MQTTLogin,可声明接收上限等。
|
||||
func MQTTLoginWith(t *testing.T, httpBase, endpointID, password string, opts MQTTLoginOpts) *MQTTSession {
|
||||
t.Helper()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package accept
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/test/harness"
|
||||
)
|
||||
|
||||
// TestF08NetemRemote 连到已经在丢包路径上的服务,确认保留消息能送达,且相同消息号不再投递一次。
|
||||
// 由虚拟机脚本准备地址。不进入 task check。
|
||||
func TestF08NetemRemote(t *testing.T) {
|
||||
if os.Getenv("NIXMSG_NETEM") != "1" {
|
||||
t.Skip("设置 NIXMSG_NETEM=1 才连到外部丢包环境")
|
||||
}
|
||||
httpBase := os.Getenv("NIXMSG_HTTP")
|
||||
tcpAddr := os.Getenv("NIXMSG_TCP")
|
||||
adminPass := os.Getenv("NIXMSG_ADMIN_PASS")
|
||||
if httpBase == "" || tcpAddr == "" || adminPass == "" {
|
||||
t.Fatal("需要 NIXMSG_HTTP、NIXMSG_TCP、NIXMSG_ADMIN_PASS")
|
||||
}
|
||||
hs := &harness.Server{
|
||||
HTTPBase: httpBase,
|
||||
AdminHTTPBase: httpBase,
|
||||
AdminPassword: adminPass,
|
||||
Addr: tcpAddr,
|
||||
}
|
||||
ac := AdminLogin(t, hs)
|
||||
const password = "password1234"
|
||||
CreateEndpoint(t, ac, "nemalice1", password)
|
||||
CreateEndpoint(t, ac, "nembob001", password)
|
||||
|
||||
alice := MQTTLoginTCP(t, tcpAddr, "nemalice1", password)
|
||||
defer alice.Close()
|
||||
bob := MQTTLoginTCP(t, tcpAddr, "nembob001", password)
|
||||
defer bob.Close()
|
||||
|
||||
send := alice.Request(t, map[string]any{
|
||||
"v": 1, "type": "send", "rid": "nem1", "id": "nem-msg-1",
|
||||
"to": map[string]any{"kind": "endpoint", "id": "nembob001"},
|
||||
"body": map[string]any{"enc": "utf8", "data": "through-loss"},
|
||||
"delay_ms": int64(0),
|
||||
"offline": map[string]any{"keep": true, "ttl_seconds": int64(3600)},
|
||||
})
|
||||
if !send.OK {
|
||||
t.Fatalf("send: %+v", send)
|
||||
}
|
||||
msg := bob.WaitType(t, "msg", 45*time.Second)
|
||||
if msg["id"] != "nem-msg-1" {
|
||||
t.Fatalf("msg: %v", msg)
|
||||
}
|
||||
again := alice.Request(t, map[string]any{
|
||||
"v": 1, "type": "send", "rid": "nem2", "id": "nem-msg-1",
|
||||
"to": map[string]any{"kind": "endpoint", "id": "nembob001"},
|
||||
"body": map[string]any{"enc": "utf8", "data": "through-loss"},
|
||||
"delay_ms": int64(0),
|
||||
"offline": map[string]any{"keep": true, "ttl_seconds": int64(3600)},
|
||||
})
|
||||
if !again.OK {
|
||||
t.Fatalf("resend same id: %+v", again)
|
||||
}
|
||||
if extra := bob.TryType("msg", 3*time.Second); extra != nil && extra["id"] == "nem-msg-1" {
|
||||
t.Fatalf("duplicate delivery: %v", extra)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package accept
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.asio.asia/nixevol/NixMsg/test/harness"
|
||||
)
|
||||
|
||||
func TestF21BareTCPLoginSendRecvAck(t *testing.T) {
|
||||
srv, err := harness.Start(harness.Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = srv.Stop() }()
|
||||
|
||||
ac := AdminLogin(t, srv)
|
||||
const password = "password1234"
|
||||
CreateEndpoint(t, ac, "tcpalice1", password)
|
||||
CreateEndpoint(t, ac, "tcpbob001", password)
|
||||
|
||||
alice := MQTTLoginTCP(t, srv.Addr, "tcpalice1", password)
|
||||
defer alice.Close()
|
||||
bob := MQTTLoginTCP(t, srv.Addr, "tcpbob001", password)
|
||||
defer bob.Close()
|
||||
|
||||
send := alice.Request(t, map[string]any{
|
||||
"v": 1, "type": "send", "rid": "tcp1", "id": "tcp-msg-1",
|
||||
"to": map[string]any{"kind": "endpoint", "id": "tcpbob001"},
|
||||
"body": map[string]any{"enc": "utf8", "data": "over-tcp"},
|
||||
"delay_ms": int64(0),
|
||||
})
|
||||
if !send.OK {
|
||||
t.Fatalf("send: %+v", send)
|
||||
}
|
||||
msg := bob.WaitType(t, "msg", 15*time.Second)
|
||||
if msg["id"] != "tcp-msg-1" {
|
||||
t.Fatalf("msg: %v", msg)
|
||||
}
|
||||
ack := bob.Request(t, map[string]any{
|
||||
"v": 1, "type": "ack", "rid": "tcp2", "from": "tcpalice1", "id": "tcp-msg-1",
|
||||
})
|
||||
if !ack.OK {
|
||||
t.Fatalf("ack: %+v", ack)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user