Files
NixMsg/internal/broker/authn_idle_test.go
T
Nixevol 8b845843d6 fix: 完成 broker 复审 B-03 至 B-12
每连接异步下发与背压、写出后断开、校验当前连接与订阅、生命周期串行、登录条件更新、闲置按在线计、认证超时并发与 Shutdown 0x8B。
2026-09-30 15:24:15 +08:00

124 lines
3.9 KiB
Go

package broker
import (
"context"
"database/sql"
"sync"
"testing"
"time"
"git.asio.asia/nixevol/NixMsg/internal/auth"
"git.asio.asia/nixevol/NixMsg/internal/store"
)
func TestSessionIdleOKUsesOnlineOffline(t *testing.T) {
now := time.UnixMilli(1_700_000_000_000)
idleDays := 1
old := now.Add(-48 * time.Hour).UnixMilli()
recentOffline := now.Add(-2 * time.Hour).UnixMilli()
if sessionIdleOK(now, old, 0, 0, idleDays) {
t.Fatal("stale used_at should expire")
}
if !sessionIdleOK(now, old, now.UnixMilli(), 0, idleDays) {
t.Fatal("currently online should not expire")
}
if !sessionIdleOK(now, old, 0, recentOffline, idleDays) {
t.Fatal("recent offline_since should keep token")
}
}
func TestPasswordLoginConditionalUpdate(t *testing.T) {
dir := t.TempDir()
db, err := store.Open(dir, "FULL")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
pool := auth.NewStubHashPool()
login := NewLogin(LoginOptions{DB: db, Pool: pool, Tokens: auth.NewSessionTokens(), Locks: auth.NewLoginLocks(), IdleDays: 30})
phc, _ := pool.Hash(context.Background(), auth.PasswordLogin, "password1")
_ = db.Queue.Do(context.Background(), func(tx *sql.Tx) error {
_, e := tx.Exec(`INSERT INTO endpoints(id, name, login_hash, talk_hash, talk_version, default_delay_ms, enabled, created_at)
VALUES ('ep-cond', '', ?, NULL, 0, 0, 1, ?)`, phc, time.Now().UnixMilli())
return e
})
res, err := login.Authenticate(context.Background(), "ep-cond", []byte("password1"), "1.1.1.1")
if err != nil || !res.OK || res.SessionToken == "" {
t.Fatalf("first login %+v err=%v", res, err)
}
ok, err := login.TokenMatchesDB(context.Background(), "ep-cond", res.SessionToken)
if err != nil || !ok {
t.Fatalf("match=%v err=%v", ok, err)
}
res2, err := login.Authenticate(context.Background(), "ep-cond", []byte("password1"), "1.1.1.1")
if err != nil || !res2.OK {
t.Fatalf("second login %+v err=%v", res2, err)
}
ok, _ = login.TokenMatchesDB(context.Background(), "ep-cond", res.SessionToken)
if ok {
t.Fatal("old token should not match after second login")
}
ok, _ = login.TokenMatchesDB(context.Background(), "ep-cond", res2.SessionToken)
if !ok {
t.Fatal("new token should match")
}
}
func TestAuthenticateRespectsCanceledContext(t *testing.T) {
dir := t.TempDir()
db, err := store.Open(dir, "FULL")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
pool := &blockingPool{ready: make(chan struct{}), release: make(chan struct{})}
login := NewLogin(LoginOptions{DB: db, Pool: pool, Tokens: auth.NewSessionTokens(), Locks: auth.NewLoginLocks()})
phc, _ := auth.NewStubHashPool().Hash(context.Background(), auth.PasswordLogin, "password1")
_ = db.Queue.Do(context.Background(), func(tx *sql.Tx) error {
_, e := tx.Exec(`INSERT INTO endpoints(id, name, login_hash, talk_hash, talk_version, default_delay_ms, enabled, created_at)
VALUES ('ep-to', '', ?, NULL, 0, 0, 1, ?)`, phc, time.Now().UnixMilli())
return e
})
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
var gotErr error
go func() {
defer wg.Done()
_, gotErr = login.Authenticate(ctx, "ep-to", []byte("password1"), "9.9.9.9")
}()
select {
case <-pool.ready:
case <-time.After(2 * time.Second):
t.Fatal("verify did not start")
}
cancel()
wg.Wait()
close(pool.release)
if gotErr == nil {
t.Fatal("expected canceled auth")
}
}
type blockingPool struct {
ready chan struct{}
release chan struct{}
once sync.Once
}
func (p *blockingPool) Hash(ctx context.Context, kind auth.PasswordKind, password string) (string, error) {
return auth.NewStubHashPool().Hash(ctx, kind, password)
}
func (p *blockingPool) Verify(ctx context.Context, _ auth.PasswordKind, _, _ string) (bool, error) {
p.once.Do(func() { close(p.ready) })
select {
case <-ctx.Done():
return false, ctx.Err()
case <-p.release:
return true, nil
}
}
func (p *blockingPool) QueueLen() int { return 0 }