material_manage_system/internal/service/websocket_ticket_test.go
文睿 张 5021b039c9 feat: 按公司隔离 WebSocket 订阅与事件投递
- 新增 POST /api/auth/websocket-ticket:需 JWT 与公司头,签发 30 秒有效的一次性随机票据
- /ws 握手消费票据并以数据库当前状态复核身份与公司授权,JWT 不进 URL
- Hub 注册/注销/投递选人统一由事件循环串行管理,消除 RLock 内 delete 的并发缺陷
- 事件携带 organization_id,仅投递同公司订阅者,投递前复核失败即关闭连接
- 统一广播签名为 (organizationID, action, orderID, data)
- 新增前端 companySocket 模块:按当前公司取票建连、切换/登出关旧连重连、消息校验组织与上下文版本
2026-10-09 13:39:45 +08:00

126 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"errors"
"testing"
"time"
)
// TestWebSocketTicketIssueBindsIdentityAndExpiry:签发的票据绑定用户、公司、签发时间,
// 并在有效期内可被一次性消费。
func TestWebSocketTicketIssueBindsIdentityAndExpiry(t *testing.T) {
base := time.Now()
svc := NewWebSocketTicketService(30 * time.Second)
svc.now = func() time.Time { return base }
ticket, err := svc.Issue(7, 2)
if err != nil {
t.Fatalf("签发失败: %v", err)
}
if ticket.UserID != 7 || ticket.OrganizationID != 2 {
t.Fatalf("票据未绑定用户/公司: %+v", ticket)
}
if !ticket.IssuedAt.Equal(base) {
t.Fatalf("签发时间错误: %v", ticket.IssuedAt)
}
if !ticket.ExpiresAt.Equal(base.Add(30 * time.Second)) {
t.Fatalf("过期时间应为签发时间 + 30s: %v", ticket.ExpiresAt)
}
// 有效期最后一刻仍可消费。
svc.now = func() time.Time { return base.Add(29 * time.Second) }
consumed, err := svc.Consume(ticket.Ticket)
if err != nil {
t.Fatalf("有效期内消费失败: %v", err)
}
if consumed.UserID != 7 || consumed.OrganizationID != 2 {
t.Fatalf("消费后身份错误: %+v", consumed)
}
}
// TestWebSocketTicketIsOneTimeOnly:票据一次性,重放或再次消费都被拒绝。
func TestWebSocketTicketIsOneTimeOnly(t *testing.T) {
svc := NewWebSocketTicketService()
ticket, err := svc.Issue(7, 2)
if err != nil {
t.Fatal(err)
}
if _, err := svc.Consume(ticket.Ticket); err != nil {
t.Fatalf("首次消费应成功: %v", err)
}
if _, err := svc.Consume(ticket.Ticket); !errors.Is(err, ErrWebSocketTicketInvalid) {
t.Fatalf("重放应被拒绝,实际 err=%v", err)
}
}
func TestWebSocketTicketRejectsUnknownAndEmpty(t *testing.T) {
for _, tc := range []struct {
name string
value string
}{
{"empty", ""},
{"unknown", "0123456789abcdef"},
} {
t.Run(tc.name, func(t *testing.T) {
svc := NewWebSocketTicketService()
if _, err := svc.Consume(tc.value); !errors.Is(err, ErrWebSocketTicketInvalid) {
t.Fatalf("应返回无效票据错误,实际 %v", err)
}
})
}
}
// TestWebSocketTicketExpires:超过 30 秒有效期的票据不能再连接。
func TestWebSocketTicketExpires(t *testing.T) {
base := time.Now()
svc := NewWebSocketTicketService(30 * time.Second)
svc.now = func() time.Time { return base }
ticket, err := svc.Issue(7, 2)
if err != nil {
t.Fatal(err)
}
svc.now = func() time.Time { return base.Add(31 * time.Second) }
if _, err := svc.Consume(ticket.Ticket); !errors.Is(err, ErrWebSocketTicketExpired) {
t.Fatalf("过期票据应被拒绝,实际 %v", err)
}
// 过期票据已从集合中移除,再次消费按“未知票据”处理。
if _, err := svc.Consume(ticket.Ticket); !errors.Is(err, ErrWebSocketTicketInvalid) {
t.Fatalf("过期后再次消费应为无效票据,实际 %v", err)
}
}
func TestWebSocketTicketIssueRequiresValidIdentity(t *testing.T) {
svc := NewWebSocketTicketService()
for _, tc := range []struct {
name string
userID, orgID int64
}{
{"zero user", 0, 2},
{"zero org", 7, 0},
{"negative", -1, -2},
} {
t.Run(tc.name, func(t *testing.T) {
if _, err := svc.Issue(tc.userID, tc.orgID); !errors.Is(err, ErrInvalidOrganizationID) {
t.Fatalf("应返回 ErrInvalidOrganizationID,实际 %v", err)
}
})
}
}
// TestWebSocketTicketValuesAreRandom:每张票据的随机标识互不相同。
func TestWebSocketTicketValuesAreRandom(t *testing.T) {
svc := NewWebSocketTicketService()
first, err := svc.Issue(7, 2)
if err != nil {
t.Fatal(err)
}
second, err := svc.Issue(7, 2)
if err != nil {
t.Fatal(err)
}
if first.Ticket == second.Ticket || len(first.Ticket) < 32 {
t.Fatalf("票据应为足够长的随机串: %q / %q", first.Ticket, second.Ticket)
}
}