Files
NixMsg/internal/auth/auth_real_test.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")
}
}