fix: 完成 broker 复审 B-03 至 B-12
每连接异步下发与背压、写出后断开、校验当前连接与订阅、生命周期串行、登录条件更新、闲置按在线计、认证超时并发与 Shutdown 0x8B。
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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 }
|
||||
Reference in New Issue
Block a user