feat(channel/session): 渠道↔会话映射 + 主人识别(actor元数据+白名单,不读消息内容) + 群聊隔离 + 主动推送策略(iLink24h/OneBot限频)
This commit is contained in:
1 parent
3ce9489f8e
commit
7adaf490c7
7 files changed
+1133
No files matched your search
@@ -0,0 +1,232 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
|
||||
)
|
||||
|
||||
// newTestManager opens a private in-memory SQLite handle and builds a manager
|
||||
// over it. Each caller gets an isolated, non-shared database so parallel tests
|
||||
// never contend.
|
||||
func newTestManager(t *testing.T) (*SessionManager, *sql.DB) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
handle, err := db.Open(ctx, ":memory:", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("db.Open(:memory:) = %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = handle.Close() })
|
||||
|
||||
m, err := New(ctx, handle, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New() = %v", err)
|
||||
}
|
||||
return m, handle
|
||||
}
|
||||
|
||||
func TestGetOrCreateCreatesAndReuses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m, _ := newTestManager(t)
|
||||
ctx := context.Background()
|
||||
msg := directMsg("o-100")
|
||||
|
||||
first, err := m.GetOrCreate(ctx, msg, testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate() = %v", err)
|
||||
}
|
||||
if first.SessionID == "" {
|
||||
t.Fatal("first SessionID empty")
|
||||
}
|
||||
if !first.Owner || !first.MemWritable {
|
||||
t.Fatalf("owner session Owner=%v MemWritable=%v, want true/true", first.Owner, first.MemWritable)
|
||||
}
|
||||
|
||||
second, err := m.GetOrCreate(ctx, msg, testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("second GetOrCreate() = %v", err)
|
||||
}
|
||||
if second.SessionID != first.SessionID {
|
||||
t.Errorf("SessionID changed across calls: %q != %q", first.SessionID, second.SessionID)
|
||||
}
|
||||
if second.ChannelID != first.ChannelID {
|
||||
t.Errorf("ChannelID changed: %q != %q", first.ChannelID, second.ChannelID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrCreateDifferentChannelsDiffer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m, _ := newTestManager(t)
|
||||
ctx := context.Background()
|
||||
|
||||
s1, err := m.GetOrCreate(ctx, directMsg("o-100"), testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate(sender1) = %v", err)
|
||||
}
|
||||
s2, err := m.GetOrCreate(ctx, directMsg("o-100"), testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate(sender2) = %v", err)
|
||||
}
|
||||
// Same real account maps to the same session, so the channel id must be the
|
||||
// same; a different account must differ.
|
||||
s3, err := m.GetOrCreate(ctx, directMsg("stranger-1"), testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate(stranger) = %v", err)
|
||||
}
|
||||
if s1.SessionID != s2.SessionID {
|
||||
t.Error("same account produced different sessions")
|
||||
}
|
||||
if s3.SessionID == s1.SessionID {
|
||||
t.Error("different account collided onto the same session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrCreateGroupIsolation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m, _ := newTestManager(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Owner speaking in a group: isolated context, memory read-only.
|
||||
sess, err := m.GetOrCreate(ctx, groupMsg("o-100"), testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate(owner group) = %v", err)
|
||||
}
|
||||
if !sess.IsGroup {
|
||||
t.Error("group message not flagged IsGroup")
|
||||
}
|
||||
if !sess.Owner {
|
||||
t.Error("owner in group should still be Owner")
|
||||
}
|
||||
if sess.MemWritable {
|
||||
t.Error("group session must be read-only (MemWritable=false) even for owner")
|
||||
}
|
||||
if got := sess.Env(); got != restrict.ActorGroupMember {
|
||||
t.Errorf("Env() = %q, want group_member", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrCreateStrangerIsolation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m, _ := newTestManager(t)
|
||||
ctx := context.Background()
|
||||
|
||||
sess, err := m.GetOrCreate(ctx, directMsg("stranger-1"), testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreate(stranger) = %v", err)
|
||||
}
|
||||
if sess.Owner {
|
||||
t.Error("stranger marked Owner")
|
||||
}
|
||||
if sess.MemWritable {
|
||||
t.Error("stranger session must be read-only (MemWritable=false)")
|
||||
}
|
||||
if sess.IsGroup {
|
||||
t.Error("stranger direct message marked IsGroup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrCreatePersistenceAcrossManagers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
handle, err := db.Open(ctx, ":memory:", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("db.Open = %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = handle.Close() })
|
||||
|
||||
m1, err := New(ctx, handle, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New#1 = %v", err)
|
||||
}
|
||||
first, err := m1.GetOrCreate(ctx, directMsg("o-100"), testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("m1.GetOrCreate = %v", err)
|
||||
}
|
||||
|
||||
// A brand-new manager over the same database must recover the same binding.
|
||||
m2, err := New(ctx, handle, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New#2 = %v", err)
|
||||
}
|
||||
second, err := m2.GetOrCreate(ctx, directMsg("o-100"), testOwner())
|
||||
if err != nil {
|
||||
t.Fatalf("m2.GetOrCreate = %v", err)
|
||||
}
|
||||
if second.SessionID != first.SessionID {
|
||||
t.Errorf("session binding lost across managers: %q != %q", first.SessionID, second.SessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrCreateConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m, _ := newTestManager(t)
|
||||
ctx := context.Background()
|
||||
msg := directMsg("o-100")
|
||||
|
||||
const n = 20
|
||||
ids := make([]string, n)
|
||||
errs := make([]error, n)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
i := i
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
sess, err := m.GetOrCreate(ctx, msg, testOwner())
|
||||
if err != nil {
|
||||
errs[i] = err
|
||||
return
|
||||
}
|
||||
ids[i] = sess.SessionID
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if errs[i] != nil {
|
||||
t.Fatalf("goroutine %d: GetOrCreate = %v", i, errs[i])
|
||||
}
|
||||
if ids[i] == "" {
|
||||
t.Fatalf("goroutine %d produced empty session id", i)
|
||||
}
|
||||
if ids[i] != ids[0] {
|
||||
t.Errorf("goroutine %d session id %q != %q", i, ids[i], ids[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrCreateInvalidChannel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m, _ := newTestManager(t)
|
||||
ctx := context.Background()
|
||||
|
||||
msg := adapter.UnifiedMessage{
|
||||
Platform: "qq",
|
||||
ChannelType: adapter.ChannelDirect,
|
||||
}
|
||||
_, err := m.GetOrCreate(ctx, msg, testOwner())
|
||||
if !errors.Is(err, ErrInvalidChannel) {
|
||||
t.Fatalf("GetOrCreate(invalid) error = %v, want ErrInvalidChannel", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRejectsNilDB(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := New(context.Background(), nil, nil); !errors.Is(err, ErrNilDB) {
|
||||
t.Fatalf("New(nil db) error = %v, want ErrNilDB", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user