141 lines
3.3 KiB
Go
141 lines
3.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPoolLimitsConcurrency(t *testing.T) {
|
|
pool := NewPoolSize(1)
|
|
errCh := make(chan error, 3)
|
|
var wg sync.WaitGroup
|
|
wg.Add(3)
|
|
for i := 0; i < 3; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
_, err := pool.Hash(context.Background(), PasswordLogin, "concurrency-test-password")
|
|
errCh <- err
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(errCh)
|
|
for err := range errCh {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if pool.MaxActive() > 1 {
|
|
t.Fatalf("max active=%d want <=1", pool.MaxActive())
|
|
}
|
|
if pool.MaxActive() < 1 {
|
|
t.Fatal("expected at least one active hash")
|
|
}
|
|
}
|
|
|
|
func TestPoolHashVerifyRoundTrip(t *testing.T) {
|
|
pool := NewPoolSize(2)
|
|
ctx := context.Background()
|
|
phc, err := pool.Hash(ctx, PasswordAdmin, "round-trip-password-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(phc, "$argon2id$") {
|
|
t.Fatalf("phc=%q", phc)
|
|
}
|
|
ok, err := pool.Verify(ctx, PasswordAdmin, "round-trip-password-1", phc)
|
|
if err != nil || !ok {
|
|
t.Fatalf("ok=%v err=%v", ok, err)
|
|
}
|
|
ok, err = pool.Verify(ctx, PasswordAdmin, "wrong-password-xxxxx", phc)
|
|
if err != nil || ok {
|
|
t.Fatalf("mismatch ok=%v err=%v", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestSessionAndAPITokens(t *testing.T) {
|
|
s := NewSessionTokens()
|
|
tok, hash, err := s.Issue(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(tok, "nst_") {
|
|
t.Fatalf("tok=%q", tok)
|
|
}
|
|
if !s.LooksLikeSessionToken(tok) {
|
|
t.Fatal("LooksLikeSessionToken")
|
|
}
|
|
if !EqualHash(hash, s.HashToken(tok)) {
|
|
t.Fatal("hash mismatch")
|
|
}
|
|
|
|
a := NewAPITokens()
|
|
atok, ahash, err := a.Issue(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(atok, "nxm_") {
|
|
t.Fatalf("atok=%q", atok)
|
|
}
|
|
if !a.LooksLikeAPIToken(atok) || !EqualHash(ahash, a.HashToken(atok)) {
|
|
t.Fatal("api token hash")
|
|
}
|
|
if EqualHash(hash, ahash) {
|
|
t.Fatal("session and api hashes should differ")
|
|
}
|
|
}
|
|
|
|
func TestLoginLocksWindowAndExpiry(t *testing.T) {
|
|
locks := NewLoginLocks()
|
|
now := time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC)
|
|
locks.SetClock(func() time.Time { return now })
|
|
|
|
key := LockKey{Kind: LockLoginEndpointIP, EndpointID: "e1", IP: "1.2.3.4"}
|
|
for i := 0; i < 9; i++ {
|
|
locked, _ := locks.Fail(key)
|
|
if locked {
|
|
t.Fatalf("locked early at %d", i+1)
|
|
}
|
|
now = now.Add(time.Second)
|
|
}
|
|
locked, retry := locks.Fail(key)
|
|
if !locked || retry <= 0 {
|
|
t.Fatalf("want lock, locked=%v retry=%v", locked, retry)
|
|
}
|
|
locked, _ = locks.Check(key)
|
|
if !locked {
|
|
t.Fatal("check should be locked")
|
|
}
|
|
|
|
now = now.Add(5*time.Minute + time.Second)
|
|
locked, _ = locks.Check(key)
|
|
if locked {
|
|
t.Fatal("should unlock after window")
|
|
}
|
|
locked, _ = locks.Fail(key)
|
|
if locked {
|
|
t.Fatal("after expiry should not still be locked on first fail")
|
|
}
|
|
}
|
|
|
|
func TestLoginLocksClearEndpoint(t *testing.T) {
|
|
locks := NewLoginLocks()
|
|
k1 := LockKey{Kind: LockLoginEndpointIP, EndpointID: "e1", IP: "1.1.1.1"}
|
|
k2 := LockKey{Kind: LockLoginEndpoint, EndpointID: "e1"}
|
|
k3 := LockKey{Kind: LockLoginEndpointIP, EndpointID: "e2", IP: "1.1.1.1"}
|
|
for i := 0; i < 10; i++ {
|
|
locks.Fail(k1)
|
|
locks.Fail(k2)
|
|
}
|
|
locks.Fail(k3)
|
|
locks.ClearEndpoint("e1")
|
|
if locked, _ := locks.Check(k1); locked {
|
|
t.Fatal("e1 ip lock should clear")
|
|
}
|
|
if locked, _ := locks.Check(k2); locked {
|
|
t.Fatal("e1 total lock should clear")
|
|
}
|
|
}
|