fix: invalidate group settings cache on membership changes
This commit is contained in:
+116
-4
@@ -58,6 +58,11 @@ type audit struct {
|
||||
usernameB string
|
||||
phoneB string
|
||||
passB string
|
||||
tokenC string
|
||||
userC string
|
||||
usernameC string
|
||||
phoneC string
|
||||
passC string
|
||||
groupTopic string
|
||||
groupVer int
|
||||
groupQR string
|
||||
@@ -110,6 +115,7 @@ func (a *audit) run() {
|
||||
suffix := fmt.Sprintf("%08d", time.Now().UnixNano()%100000000)
|
||||
a.usernameA, a.phoneA, a.passA = "a"+suffix, "155"+suffix, "AuditPass123"
|
||||
a.usernameB, a.phoneB, a.passB = "b"+suffix, "156"+suffix, "AuditPass123"
|
||||
a.usernameC, a.phoneC, a.passC = "c"+suffix, "159"+suffix, "AuditPass123"
|
||||
|
||||
a.testHTTP("1", "1.1", "企业码解析", "POST", "/v1/im/tenants/resolve", map[string]any{"tenant_code": a.tenantCode, "platform": "ios", "app_version": "1.0.0"}, "", true, []int{200}, func(r imResp) string {
|
||||
return fmt.Sprintf("tenant=%v endpoints=%v", dig(r.Data, "tenant", "code"), dig(r.Data, "endpoints", "api"))
|
||||
@@ -217,10 +223,58 @@ func (a *audit) run() {
|
||||
a.testHTTP("6", "6.join-requests", "入群申请列表", "GET", groupPath+"/join-requests?status=pending&cursor=", nil, a.tokenA, false, []int{200, 404}, nil)
|
||||
a.testHTTP("6", "6.summary", "群概要", "GET", groupPath+"/summary", nil, a.tokenA, false, []int{200, 404}, nil)
|
||||
a.testHTTP("6", "6.ann-latest", "群公告最新", "GET", groupPath+"/announcements/latest", nil, a.tokenA, false, []int{200, 404}, nil)
|
||||
a.testHTTP("6", "6.ann-put", "发布群公告", "PUT", groupPath+"/announcements/current", map[string]any{"content": "联调公告", "version": 1}, a.tokenA, true, []int{200, 409, 404}, nil)
|
||||
a.testHTTP("6", "6.governance", "群治理配置", "GET", groupPath+"/governance", nil, a.tokenA, false, []int{200, 404}, nil)
|
||||
a.testHTTP("6", "6.governance-patch", "更新群治理配置", "PATCH", groupPath+"/governance", map[string]any{"join_approval": true, "member_invite": true, "version": 1}, a.tokenA, true, []int{200, 409, 404}, nil)
|
||||
announcementID := ""
|
||||
a.testHTTP("6", "6.ann-put", "发布群公告", "PUT", groupPath+"/announcements/current", map[string]any{"content": "联调公告", "version": 1}, a.tokenA, true, []int{200}, func(r imResp) string {
|
||||
announcementID = asString(r.Data["announcement_id"])
|
||||
return "announcement_id=" + announcementID
|
||||
})
|
||||
if announcementID != "" {
|
||||
a.testHTTP("6", "6.ann-detail", "群公告详情", "GET", groupPath+"/announcements/"+url.PathEscape(announcementID), nil, a.tokenB, false, []int{200}, nil)
|
||||
a.testHTTP("6", "6.ann-read", "群公告已读", "POST", groupPath+"/announcements/"+url.PathEscape(announcementID)+"/read", map[string]any{}, a.tokenB, true, []int{200}, nil)
|
||||
}
|
||||
a.testHTTP("6", "6.governance", "群治理配置", "GET", groupPath+"/governance", nil, a.tokenA, false, []int{200}, func(r imResp) string {
|
||||
if version := asInt(r.Data["version"]); version > 0 {
|
||||
a.groupVer = version
|
||||
}
|
||||
return fmt.Sprintf("version=%d", a.groupVer)
|
||||
})
|
||||
a.testHTTP("6", "6.governance-member-forbidden", "普通成员更新群治理被拒绝", "PATCH", groupPath+"/governance", map[string]any{"join_approval": true, "version": a.groupVer}, a.tokenB, true, []int{403}, nil)
|
||||
a.testHTTP("6", "6.governance-patch", "更新群治理配置成功", "PATCH", groupPath+"/governance", map[string]any{"join_approval": true, "member_invite": true, "member_upload_files": true, "version": a.groupVer}, a.tokenA, true, []int{200}, func(r imResp) string {
|
||||
if version := asInt(r.Data["version"]); version > 0 {
|
||||
a.groupVer = version
|
||||
}
|
||||
return fmt.Sprintf("version=%d", a.groupVer)
|
||||
})
|
||||
a.testHTTP("6", "6.role-limits", "管理员数量限制", "GET", groupPath+"/role-limits", nil, a.tokenA, false, []int{200, 404}, nil)
|
||||
a.testHTTP("6", "6.role-limits-member-forbidden", "普通成员读取管理员限制被拒绝", "GET", groupPath+"/role-limits", nil, a.tokenB, false, []int{403}, nil)
|
||||
a.testHTTP("6", "6.mute-member-forbidden", "普通成员禁言他人被拒绝", "POST", groupPath+"/mutes/batch", map[string]any{"user_ids": []string{a.userA}, "reason": "越权测试"}, a.tokenB, true, []int{403}, nil)
|
||||
a.testHTTP("6", "6.mute-member", "群主禁言成员", "POST", groupPath+"/mutes/batch", map[string]any{"user_ids": []string{a.userB}, "reason": "联调禁言"}, a.tokenA, true, []int{200}, nil)
|
||||
a.testHTTP("6", "6.mute-self-view", "成员查看自身禁言状态", "GET", groupPath+"/mutes?cursor=", nil, a.tokenB, false, []int{200}, func(r imResp) string {
|
||||
return fmt.Sprintf("items=%v", r.Data["items"])
|
||||
})
|
||||
a.testHTTP("6", "6.unmute-member", "群主解除成员禁言", "DELETE", groupPath+"/mutes/"+url.PathEscape(a.userB), nil, a.tokenA, true, []int{200}, nil)
|
||||
a.testHTTP("6", "6.admin-add", "群主设置管理员", "POST", groupPath+"/admins/batch", map[string]any{"user_ids": []string{a.userB}, "version": a.groupVer}, a.tokenA, true, []int{200}, nil)
|
||||
a.testHTTP("6", "6.admin-governance", "管理员更新群治理", "PATCH", groupPath+"/governance", map[string]any{"member_private_chat": true, "version": a.groupVer}, a.tokenB, true, []int{200, 409}, func(r imResp) string {
|
||||
if version := asInt(r.Data["version"]); version > 0 {
|
||||
a.groupVer = version
|
||||
}
|
||||
return fmt.Sprintf("version=%d", a.groupVer)
|
||||
})
|
||||
a.testHTTP("6", "6.admin-remove", "群主取消管理员", "DELETE", groupPath+"/admins/"+url.PathEscape(a.userB), nil, a.tokenA, true, []int{200}, nil)
|
||||
a.registerThird("6", "6.join-user", a.usernameC, a.phoneC, a.passC)
|
||||
joinRequestID, joinRequestVersion := "", 0
|
||||
a.testHTTP("6", "6.join-create", "非成员提交入群申请", "POST", groupPath+"/join-requests", map[string]any{"message": "申请加入联调群", "source": "manual"}, a.tokenC, true, []int{200}, func(r imResp) string {
|
||||
joinRequestID = asString(r.Data["request_id"])
|
||||
joinRequestVersion = asInt(r.Data["version"])
|
||||
return fmt.Sprintf("request_id=%s version=%d", joinRequestID, joinRequestVersion)
|
||||
})
|
||||
a.testHTTP("6", "6.join-list-member-forbidden", "普通成员读取入群申请被拒绝", "GET", groupPath+"/join-requests?status=pending&cursor=", nil, a.tokenB, false, []int{403}, nil)
|
||||
a.testHTTP("6", "6.join-list-owner", "群主读取待审批申请", "GET", groupPath+"/join-requests?status=pending&cursor=", nil, a.tokenA, false, []int{200}, nil)
|
||||
if joinRequestID != "" {
|
||||
a.testHTTP("6", "6.join-accept", "群主通过入群申请", "POST", groupPath+"/join-requests/"+url.PathEscape(joinRequestID)+"/accept", map[string]any{"version": joinRequestVersion}, a.tokenA, true, []int{200}, nil)
|
||||
a.testHTTP("6", "6.join-detail", "审批后新成员读取群详情", "GET", groupPath, nil, a.tokenC, false, []int{200}, nil)
|
||||
a.testHTTP("6", "6.join-leave", "新成员主动退群", "POST", groupPath+"/leave", map[string]any{}, a.tokenC, true, []int{200}, nil)
|
||||
}
|
||||
a.testHTTP("6", "6.admins", "管理员列表", "GET", groupPath+"/members?role=admin&cursor=", nil, a.tokenA, false, []int{200, 404}, nil)
|
||||
a.testHTTP("6", "6.mutes", "禁言列表", "GET", groupPath+"/mutes?cursor=", nil, a.tokenA, false, []int{200, 404}, nil)
|
||||
a.testHTTP("6", "6.files", "群文件列表", "GET", groupPath+"/files?query=&type=&cursor=", nil, a.tokenA, false, []int{200, 404, 501}, nil)
|
||||
@@ -309,6 +363,17 @@ func (a *audit) run() {
|
||||
return "category=" + a.feedbackCat
|
||||
})
|
||||
a.testHTTP("11", "11.feedback", "提交反馈", "POST", "/v1/im/feedback", map[string]any{"category": a.feedbackCat, "content": "自动化联调反馈", "contact": "", "attachment_file_ids": []string{}, "client_context": map[string]any{"source": "imtestaudit"}, "include_diagnostics": true}, a.tokenA, true, []int{200}, nil)
|
||||
a.testHTTP("6", "6.owner-version", "转让前读取群版本", "GET", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/governance", nil, a.tokenA, false, []int{200}, func(r imResp) string {
|
||||
if version := asInt(r.Data["version"]); version > 0 {
|
||||
a.groupVer = version
|
||||
}
|
||||
return fmt.Sprintf("version=%d", a.groupVer)
|
||||
})
|
||||
transferToken := a.securityActionToken("6", "6.owner-transfer", "group_owner_transfer", a.tokenA)
|
||||
a.testHTTPHeaders("6", "6.owner-transfer-action", "群主转让", "POST", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/owner-transfer", map[string]any{"target_user_id": a.userB, "version": a.groupVer}, a.tokenA, true, []int{200}, map[string]string{"X-Action-Token": transferToken}, nil)
|
||||
dissolveToken := a.securityActionToken("6", "6.dissolve", "group_dissolve", a.tokenB)
|
||||
a.testHTTPHeaders("6", "6.dissolve-action", "新群主解散群", "DELETE", "/v1/im/groups/"+url.PathEscape(a.groupTopic), map[string]any{"reason": "自动化验收完成"}, a.tokenB, true, []int{200}, map[string]string{"X-Action-Token": dissolveToken}, nil)
|
||||
a.testHTTP("6", "6.dissolved-detail", "解散后群详情返回 Gone", "GET", "/v1/im/groups/"+url.PathEscape(a.groupTopic), nil, a.tokenA, false, []int{410}, nil)
|
||||
a.testHTTP("1", "1.logout", "退出登录", "POST", "/v1/im/auth/logout", map[string]any{}, a.tokenA, true, []int{200, 404, 405}, nil)
|
||||
}
|
||||
|
||||
@@ -337,6 +402,27 @@ func (a *audit) register(chapter, id, username, phone, password string, primary
|
||||
}
|
||||
}
|
||||
|
||||
func (a *audit) registerThird(chapter, id, username, phone, password string) {
|
||||
var challenge, verification string
|
||||
a.testHTTP(chapter, id+"-code", "入群用户注册发码 "+username, "POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "phone": phone}, "", true, []int{200}, func(r imResp) string {
|
||||
challenge = asString(r.Data["challenge_id"])
|
||||
return "challenge_id=" + mask(challenge)
|
||||
})
|
||||
if challenge != "" {
|
||||
a.testHTTP(chapter, id+"-verify", "入群用户验证码校验 "+username, "POST", "/v1/im/auth/verification-codes/"+url.PathEscape(challenge)+"/verify", map[string]any{"code": "123456"}, "", true, []int{200}, func(r imResp) string {
|
||||
verification = asString(r.Data["verification_token"])
|
||||
return "verification_token=" + mask(verification)
|
||||
})
|
||||
}
|
||||
token, userID, note, err := a.wsRegister(username, phone, password, challenge, verification)
|
||||
res := result{Chapter: chapter, ID: id + "-ws-acc", Name: "入群用户 WebSocket 注册 " + username, Method: "WS", Path: "/v0/channels", Pass: err == nil, Note: note}
|
||||
if err != nil {
|
||||
res.Reason = err.Error()
|
||||
}
|
||||
a.results = append(a.results, res)
|
||||
a.tokenC, a.userC = token, userID
|
||||
}
|
||||
|
||||
func (a *audit) wsRegister(username, phone, password, challenge, verification string) (string, string, string, error) {
|
||||
u, _ := url.Parse(a.wsURL)
|
||||
q := u.Query()
|
||||
@@ -408,7 +494,11 @@ func waitCtrl(conn *websocket.Conn, id string) (map[string]any, error) {
|
||||
}
|
||||
|
||||
func (a *audit) testHTTP(chapter, id, name, method, path string, body any, token string, idem bool, want []int, note func(imResp) string) {
|
||||
status, parsed, raw, err := a.call(method, path, body, token, idem)
|
||||
a.testHTTPHeaders(chapter, id, name, method, path, body, token, idem, want, nil, note)
|
||||
}
|
||||
|
||||
func (a *audit) testHTTPHeaders(chapter, id, name, method, path string, body any, token string, idem bool, want []int, headers map[string]string, note func(imResp) string) {
|
||||
status, parsed, raw, err := a.callHeaders(method, path, body, token, idem, headers)
|
||||
r := result{Chapter: chapter, ID: id, Name: name, Method: method, Path: path, Status: status, Code: parsed.Code, Reason: parsed.Reason, Body: compact(raw)}
|
||||
if err != nil {
|
||||
r.Reason = err.Error()
|
||||
@@ -427,6 +517,10 @@ func (a *audit) testHTTP(chapter, id, name, method, path string, body any, token
|
||||
}
|
||||
|
||||
func (a *audit) call(method, path string, body any, token string, idem bool) (int, imResp, string, error) {
|
||||
return a.callHeaders(method, path, body, token, idem, nil)
|
||||
}
|
||||
|
||||
func (a *audit) callHeaders(method, path string, body any, token string, idem bool, headers map[string]string) (int, imResp, string, error) {
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
b, _ := json.Marshal(body)
|
||||
@@ -458,6 +552,9 @@ func (a *audit) call(method, path string, body any, token string, idem bool) (in
|
||||
if idem {
|
||||
req.Header.Set("Idempotency-Key", "audit-"+fmt.Sprint(time.Now().UnixNano()))
|
||||
}
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, imResp{}, "", err
|
||||
@@ -469,6 +566,21 @@ func (a *audit) call(method, path string, body any, token string, idem bool) (in
|
||||
return resp.StatusCode, parsed, string(b), nil
|
||||
}
|
||||
|
||||
func (a *audit) securityActionToken(chapter, id, scene, token string) string {
|
||||
challengeID, actionToken := "", ""
|
||||
a.testHTTP(chapter, id+"-challenge", scene+" 二次验证发起", "POST", "/v1/im/security/challenges", map[string]any{"scene": scene, "channel": "sms"}, token, true, []int{200}, func(r imResp) string {
|
||||
challengeID = asString(r.Data["challenge_id"])
|
||||
return "challenge_id=" + mask(challengeID)
|
||||
})
|
||||
if challengeID != "" {
|
||||
a.testHTTP(chapter, id+"-verify", scene+" 二次验证码校验", "POST", "/v1/im/security/challenges/"+url.PathEscape(challengeID)+"/verify", map[string]any{"code": "123456"}, token, true, []int{200}, func(r imResp) string {
|
||||
actionToken = asString(r.Data["action_token"])
|
||||
return "action_token=" + mask(actionToken)
|
||||
})
|
||||
}
|
||||
return actionToken
|
||||
}
|
||||
|
||||
func (a *audit) report() string {
|
||||
var b strings.Builder
|
||||
now := time.Now().Format("2006-01-02 15:04:05 -0700")
|
||||
|
||||
+168
-1
@@ -55,6 +55,8 @@ type audit struct {
|
||||
p2pTopic string
|
||||
bobTopic string
|
||||
groupTopic string
|
||||
groupSeq int
|
||||
groupFile string
|
||||
fileURL string
|
||||
messageID string
|
||||
}
|
||||
@@ -330,9 +332,26 @@ func (a *audit) run() {
|
||||
return "topic=" + topic
|
||||
})
|
||||
a.step("WS-21", "A/B 订阅群 Topic", "WS", a.groupTopic, func() string {
|
||||
if alice == nil || bob == nil || a.groupTopic == "" {
|
||||
if a.groupTopic == "" {
|
||||
return "FAIL: websocket client or group missing"
|
||||
}
|
||||
// Reconnect before the group-specific checks. A read timeout permanently
|
||||
// poisons a gorilla/websocket connection and must not cascade into this section.
|
||||
if alice != nil && alice.conn != nil {
|
||||
_ = alice.conn.Close()
|
||||
}
|
||||
if bob != nil && bob.conn != nil {
|
||||
_ = bob.conn.Close()
|
||||
}
|
||||
var err error
|
||||
alice, err = a.openLoggedIn("A-group", a.userA, a.tokenA)
|
||||
if err != nil {
|
||||
return "FAIL: A reconnect: " + err.Error()
|
||||
}
|
||||
bob, err = a.openLoggedIn("B-group", a.userB, a.tokenB)
|
||||
if err != nil {
|
||||
return "FAIL: B reconnect: " + err.Error()
|
||||
}
|
||||
if err := alice.sub(a.groupTopic); err != nil {
|
||||
return "FAIL: A sub group: " + err.Error()
|
||||
}
|
||||
@@ -350,6 +369,112 @@ func (a *audit) run() {
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
a.groupSeq = seq
|
||||
return fmt.Sprintf("seq=%d topic=%s", seq, topic)
|
||||
})
|
||||
a.step("WS-22E", "群消息 client_mid 幂等去重", "WS", a.groupTopic, func() string {
|
||||
if alice == nil {
|
||||
return "FAIL: websocket client missing"
|
||||
}
|
||||
head := map[string]any{"mime": "text/plain", "x-im-client-mid": "dedup-" + suffix}
|
||||
first, err := alice.pubCtrl(a.groupTopic, head, "群消息幂等测试 "+suffix)
|
||||
if err != nil {
|
||||
return "FAIL: first pub: " + err.Error()
|
||||
}
|
||||
second, err := alice.pubCtrl(a.groupTopic, head, "群消息幂等测试 "+suffix)
|
||||
if err != nil {
|
||||
return "FAIL: duplicate pub: " + err.Error()
|
||||
}
|
||||
if first <= 0 || second != first {
|
||||
return fmt.Sprintf("FAIL: first_seq=%d duplicate_seq=%d", first, second)
|
||||
}
|
||||
return fmt.Sprintf("first_seq=%d duplicate_seq=%d", first, second)
|
||||
})
|
||||
a.step("WS-22F", "群消息上下文查询", "HTTP", "/v1/im/topics/{topic}/messages/{seq}/context", func() string {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
status, r, err := a.call("GET", fmt.Sprintf("/v1/im/topics/%s/messages/%d/context?before=5&after=5", url.PathEscape(a.groupTopic), a.groupSeq), nil, a.tokenB)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return fmt.Sprintf("FAIL: status=%d reason=%s", status, r.Reason)
|
||||
}
|
||||
anchor, count := asInt(r.Data["anchor_seq"]), len(asSlice(r.Data["items"]))
|
||||
if anchor != a.groupSeq || count == 0 {
|
||||
return fmt.Sprintf("FAIL: anchor_seq=%d items=%d", anchor, count)
|
||||
}
|
||||
return fmt.Sprintf("anchor_seq=%d items=%d", anchor, count)
|
||||
})
|
||||
a.step("WS-22G", "群聊天记录搜索", "HTTP", "/v1/im/topics/{topic}/messages/search", func() string {
|
||||
status, r, err := a.call("GET", "/v1/im/topics/"+url.PathEscape(a.groupTopic)+"/messages/search?q="+url.QueryEscape("WS 群消息")+"&type=text", nil, a.tokenB)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return fmt.Sprintf("FAIL: status=%d reason=%s", status, r.Reason)
|
||||
}
|
||||
count := len(asSlice(r.Data["items"]))
|
||||
if count == 0 {
|
||||
return "FAIL: items=0"
|
||||
}
|
||||
return fmt.Sprintf("items=%d", count)
|
||||
})
|
||||
a.step("WS-22H", "群消息发送者撤回", "HTTP", "/v1/im/topics/{topic}/messages/{seq}/recall", func() string {
|
||||
status, r, err := a.call("POST", fmt.Sprintf("/v1/im/topics/%s/messages/%d/recall", url.PathEscape(a.groupTopic), a.groupSeq), map[string]any{"version": 1, "reason": "WS 群消息撤回联调"}, a.tokenA)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return fmt.Sprintf("FAIL: status=%d reason=%s", status, r.Reason)
|
||||
}
|
||||
return fmt.Sprintf("seq=%d status=%s version=%d", asInt(r.Data["seq"]), asString(r.Data["status"]), asInt(r.Data["version"]))
|
||||
})
|
||||
a.step("WS-22A", "HTTP 禁言群成员 B", "HTTP", "/v1/im/groups/{topic}/mutes/batch", func() string {
|
||||
status, r, err := a.call("POST", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/mutes/batch", map[string]any{
|
||||
"user_ids": []string{a.userB}, "reason": "WS 禁言策略联调",
|
||||
}, a.tokenA)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return fmt.Sprintf("FAIL: status=%d reason=%s", status, r.Reason)
|
||||
}
|
||||
return "member B muted"
|
||||
})
|
||||
a.step("WS-22B", "被禁言成员 B pub 被拒绝", "WS", a.groupTopic, func() string {
|
||||
if bob == nil {
|
||||
return "FAIL: websocket client missing"
|
||||
}
|
||||
id := "pub-muted-" + suffix
|
||||
code, text, err := bob.sendCtrl(id, map[string]any{"pub": map[string]any{
|
||||
"id": id, "topic": a.groupTopic, "head": map[string]any{"mime": "text/plain"}, "content": "本消息不应发出",
|
||||
}}, 15*time.Second)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
if code != http.StatusForbidden {
|
||||
return fmt.Sprintf("FAIL: unexpected code=%d text=%s", code, text)
|
||||
}
|
||||
return fmt.Sprintf("code=%d text=%s", code, text)
|
||||
})
|
||||
a.step("WS-22C", "HTTP 解除群成员 B 禁言", "HTTP", "/v1/im/groups/{topic}/mutes/{user_id}", func() string {
|
||||
status, r, err := a.call("DELETE", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/mutes/"+url.PathEscape(a.userB), nil, a.tokenA)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return fmt.Sprintf("FAIL: status=%d reason=%s", status, r.Reason)
|
||||
}
|
||||
return "member B unmuted"
|
||||
})
|
||||
a.step("WS-22D", "解除禁言后 B pub,A 收到 data", "WS", a.groupTopic, func() string {
|
||||
if alice == nil || bob == nil {
|
||||
return "FAIL: websocket client missing"
|
||||
}
|
||||
seq, topic, err := bob.pubWithHeadAndPeer(alice, a.groupTopic, map[string]any{"mime": "text/plain", "x-im-client-mid": "unmute-" + suffix}, "解除禁言消息 "+suffix)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
return fmt.Sprintf("seq=%d topic=%s", seq, topic)
|
||||
})
|
||||
a.step("WS-23", "get 群 desc/sub/data", "WS", a.groupTopic, func() string {
|
||||
@@ -369,6 +494,43 @@ func (a *audit) run() {
|
||||
a.fileURL = fileURL
|
||||
return "url=" + mask(fileURL)
|
||||
})
|
||||
a.step("WS-24A", "群文件关联 OSS 上传文件", "HTTP", "/v1/im/groups/{topic}/files", func() string {
|
||||
status, r, err := a.call("POST", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/files", map[string]any{
|
||||
"file_id": a.fileURL, "file_name": "imwsaudit.txt", "mime_type": "text/plain", "file_type": "document",
|
||||
}, a.tokenA)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return fmt.Sprintf("FAIL: status=%d reason=%s", status, r.Reason)
|
||||
}
|
||||
a.groupFile = asString(r.Data["group_file_id"])
|
||||
if a.groupFile == "" {
|
||||
return "FAIL: missing group_file_id"
|
||||
}
|
||||
return "group_file_id=" + a.groupFile
|
||||
})
|
||||
a.step("WS-24B", "群文件列表可查询", "HTTP", "/v1/im/groups/{topic}/files", func() string {
|
||||
status, r, err := a.call("GET", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/files?query=imwsaudit&type=document", nil, a.tokenB)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
count := len(asSlice(r.Data["items"]))
|
||||
if status != http.StatusOK || count == 0 {
|
||||
return fmt.Sprintf("FAIL: status=%d reason=%s items=%d", status, r.Reason, count)
|
||||
}
|
||||
return fmt.Sprintf("items=%d", count)
|
||||
})
|
||||
a.step("WS-24C", "群主删除群文件关联", "HTTP", "/v1/im/groups/{topic}/files/{group_file_id}", func() string {
|
||||
status, r, err := a.call("DELETE", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/files/"+url.PathEscape(a.groupFile), nil, a.tokenA)
|
||||
if err != nil {
|
||||
return "FAIL: " + err.Error()
|
||||
}
|
||||
if status != http.StatusOK || asString(r.Data["status"]) != "deleted" {
|
||||
return fmt.Sprintf("FAIL: status=%d reason=%s file_status=%s", status, r.Reason, asString(r.Data["status"]))
|
||||
}
|
||||
return "status=deleted"
|
||||
})
|
||||
a.step("WS-25", "WS 文件消息下发", "WS", a.p2pTopic, func() string {
|
||||
if alice == nil || bob == nil || a.fileURL == "" {
|
||||
return "FAIL: websocket client or file missing"
|
||||
@@ -1028,6 +1190,11 @@ func asInt(v any) int {
|
||||
}
|
||||
}
|
||||
|
||||
func asSlice(v any) []any {
|
||||
items, _ := v.([]any)
|
||||
return items
|
||||
}
|
||||
|
||||
func mask(s string) string {
|
||||
if len(s) <= 8 {
|
||||
return s
|
||||
|
||||
Reference in New Issue
Block a user