- 新增 POST /api/auth/websocket-ticket:需 JWT 与公司头,签发 30 秒有效的一次性随机票据 - /ws 握手消费票据并以数据库当前状态复核身份与公司授权,JWT 不进 URL - Hub 注册/注销/投递选人统一由事件循环串行管理,消除 RLock 内 delete 的并发缺陷 - 事件携带 organization_id,仅投递同公司订阅者,投递前复核失败即关闭连接 - 统一广播签名为 (organizationID, action, orderID, data) - 新增前端 companySocket 模块:按当前公司取票建连、切换/登出关旧连重连、消息校验组织与上下文版本
126 lines
3.7 KiB
Go
126 lines
3.7 KiB
Go
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)
|
||
}
|
||
}
|