Evict/cap limiter keys, replace hard username lockouts with IP+user progressive delays cleared on success, and create bootstrap admins under the same advisory/mutex lock as role changes.
102 lines
2.1 KiB
Go
102 lines
2.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestMemoryConcurrentLastAdminDemotion(t *testing.T) {
|
|
m := NewMemory()
|
|
ctx := context.Background()
|
|
a := &User{Username: "admin_a", PasswordHash: "x", Role: RoleUser}
|
|
b := &User{Username: "admin_b", PasswordHash: "x", Role: RoleUser}
|
|
if err := m.CreateUser(ctx, a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.CreateUser(ctx, b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.SetUserRole(ctx, a.ID, RoleAdmin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.SetUserRole(ctx, b.ID, RoleAdmin); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 2)
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
errs <- m.SetUserRole(ctx, a.ID, RoleUser)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
errs <- m.SetUserRole(ctx, b.ID, RoleUser)
|
|
}()
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
var ok, lastAdmin int
|
|
for err := range errs {
|
|
switch err {
|
|
case nil:
|
|
ok++
|
|
case ErrLastAdmin:
|
|
lastAdmin++
|
|
default:
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
if ok != 1 || lastAdmin != 1 {
|
|
t.Fatalf("want 1 success and 1 ErrLastAdmin, got ok=%d lastAdmin=%d", ok, lastAdmin)
|
|
}
|
|
n, err := m.CountAdmins(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("admins remaining = %d, want 1", n)
|
|
}
|
|
}
|
|
|
|
func TestMemoryConcurrentBootstrapAdmin(t *testing.T) {
|
|
m := NewMemory()
|
|
ctx := context.Background()
|
|
a := &User{Username: "boot_a", PasswordHash: "x", Role: RoleAdmin}
|
|
b := &User{Username: "boot_b", PasswordHash: "x", Role: RoleAdmin}
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 2)
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
errs <- m.CreateUser(ctx, a)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
errs <- m.CreateUser(ctx, b)
|
|
}()
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
n, err := m.CountAdmins(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("bootstrap race left %d admins, want 1", n)
|
|
}
|
|
if a.Role == RoleAdmin && b.Role == RoleAdmin {
|
|
t.Fatal("both users kept RoleAdmin")
|
|
}
|
|
if a.Role != RoleAdmin && b.Role != RoleAdmin {
|
|
t.Fatal("neither user is admin")
|
|
}
|
|
}
|