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 }