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) } }