Files
DockMV/internal/store/users_test.go
T
kawaandClaude Sonnet 5 8d20d9be84
Sync Gitea releases to GitHub / sync-releases (push) Canceled after 0s
Feature 1: Local accounts, replacing shared token
- Local accounts with bcrypt password hashing; first-run setup via POST /api/setup
- Personal API tokens (dmv_<48hex>, SHA-256 hashed at rest) for scripted access
- Server-side in-memory sessions with 32-byte secure cookie (dockmv_session, 7-day TTL, sliding renewal)
- Login rate limiting (exponential backoff 1s–30s cap) per IP
- Refuse to bind non-loopback while no account exists, unless DOCKMV_TRUST_ADDR=1 (for Docker's port mapping)
- Every account can manage every other account (no roles in v1)
- Auth middleware: public-path allowlist (/api/setup, /api/login, /api/logout, /api/me, /api/health) + session cookie check + API token (X-Auth-Token or Authorization: Bearer) check
- Frontend AuthGate gates app on GET /api/me; shows setup screen or login form or app tree as needed
- Account tab for personal token management; sign-out button in topbar
- Break: removed --token flag, DOCKMV_TOKEN env var, ?token= query param, /api/health no longer auto-responds when unauthenticated

Verified: go build/vet clean, frontend tsc+vite clean. Sandbox cannot execute binaries to test setup→login→session→token flow at runtime; recommend manual pass before merge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 15:13:54 +02:00

132 lines
3.3 KiB
Go

package store
import (
"path/filepath"
"strings"
"testing"
)
func newTestUsers(t *testing.T) (*Users, string) {
t.Helper()
path := filepath.Join(t.TempDir(), "users.json")
u, err := NewUsers(path)
if err != nil {
t.Fatalf("NewUsers: %v", err)
}
return u, path
}
func TestUsersCreateAndVerify(t *testing.T) {
u, _ := newTestUsers(t)
usr, err := u.Create("Alice", "correct-password")
if err != nil {
t.Fatalf("Create: %v", err)
}
if usr.PasswordHash != "" {
t.Fatal("Create must return a redacted user")
}
if usr.ID == "" {
t.Fatal("an id should have been generated")
}
if _, err := u.Verify("alice", "correct-password"); err != nil {
t.Fatalf("Verify with matching case-insensitive username: %v", err)
}
if _, err := u.Verify("Alice", "wrong-password"); err != ErrInvalidCredentials {
t.Fatalf("Verify with wrong password = %v, want ErrInvalidCredentials", err)
}
if _, err := u.Verify("nobody", "correct-password"); err != ErrInvalidCredentials {
t.Fatalf("Verify with unknown username = %v, want ErrInvalidCredentials", err)
}
}
func TestUsersUsernameUniqueCaseInsensitive(t *testing.T) {
u, _ := newTestUsers(t)
if _, err := u.Create("Bob", "password1"); err != nil {
t.Fatalf("Create: %v", err)
}
if _, err := u.Create("bob", "password2"); err != ErrUsernameTaken {
t.Fatalf("Create with case-different duplicate = %v, want ErrUsernameTaken", err)
}
}
func TestUsersValidation(t *testing.T) {
u, _ := newTestUsers(t)
if _, err := u.Create("", "password"); err == nil {
t.Fatal("empty username should be rejected")
}
if _, err := u.Create("carol", ""); err == nil {
t.Fatal("empty password should be rejected")
}
if _, err := u.Create("dave", strings.Repeat("x", 73)); err != ErrPasswordTooLong {
t.Fatalf("73-byte password = %v, want ErrPasswordTooLong", err)
}
}
func TestUsersDeleteRefusesLastAccount(t *testing.T) {
u, _ := newTestUsers(t)
a, err := u.Create("alice", "password1")
if err != nil {
t.Fatalf("Create: %v", err)
}
b, err := u.Create("bob", "password2")
if err != nil {
t.Fatalf("Create: %v", err)
}
if err := u.Delete(a.ID); err != nil {
t.Fatalf("Delete first account: %v", err)
}
if err := u.Delete(b.ID); err != ErrLastAccount {
t.Fatalf("Delete last account = %v, want ErrLastAccount", err)
}
if u.Count() != 1 {
t.Fatalf("Count = %d, want 1", u.Count())
}
}
func TestUsersPersistAcrossReload(t *testing.T) {
u, path := newTestUsers(t)
if _, err := u.Create("alice", "correct-password"); err != nil {
t.Fatalf("Create: %v", err)
}
reopened, err := NewUsers(path)
if err != nil {
t.Fatalf("NewUsers: %v", err)
}
if reopened.Count() != 1 {
t.Fatalf("Count after reload = %d, want 1", reopened.Count())
}
if _, err := reopened.Verify("alice", "correct-password"); err != nil {
t.Fatalf("Verify after reload: %v", err)
}
}
func TestUsersTouchLastLogin(t *testing.T) {
u, _ := newTestUsers(t)
usr, err := u.Create("alice", "correct-password")
if err != nil {
t.Fatalf("Create: %v", err)
}
if usr.LastLoginAt != nil {
t.Fatal("a fresh account should have no last login")
}
if err := u.TouchLastLogin(usr.ID); err != nil {
t.Fatalf("TouchLastLogin: %v", err)
}
got, err := u.Get(usr.ID)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got.LastLoginAt == nil {
t.Fatal("LastLoginAt should be set after TouchLastLogin")
}
}