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") } }