Files
synapbus/internal/auth/user_store_test.go
Algis DumbrisandClaude Opus 4.6 cad0138337 chore: migrate to github.com/synapbus org and add logo assets
Move module path from github.com/smart-mcp-proxy/synapbus to
github.com/synapbus/synapbus across all Go imports (47 files).
Add constellation logo options generated via FLUX 1.1 Pro.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 05:46:46 +02:00

290 lines
7.2 KiB
Go

package auth
import (
"context"
"database/sql"
"fmt"
"testing"
_ "modernc.org/sqlite"
"github.com/synapbus/synapbus/internal/storage"
"golang.org/x/crypto/bcrypt"
)
func newTestDB(t *testing.T) *sql.DB {
t.Helper()
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatalf("open database: %v", err)
}
t.Cleanup(func() { db.Close() })
if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
t.Fatalf("enable foreign keys: %v", err)
}
ctx := context.Background()
if err := storage.RunMigrations(ctx, db); err != nil {
t.Fatalf("run migrations: %v", err)
}
return db
}
func TestUserStore_CreateUser(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10) // use cost 10 for test speed
ctx := context.Background()
t.Run("valid user", func(t *testing.T) {
user, err := store.CreateUser(ctx, "testuser", "password123", "Test User")
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
if user.ID == 0 {
t.Error("expected non-zero user ID")
}
if user.Username != "testuser" {
t.Errorf("Username = %q, want %q", user.Username, "testuser")
}
if user.DisplayName != "Test User" {
t.Errorf("DisplayName = %q, want %q", user.DisplayName, "Test User")
}
// First user should be admin
if user.Role != RoleAdmin {
t.Errorf("Role = %q, want %q (first user should be admin)", user.Role, RoleAdmin)
}
})
t.Run("second user is regular", func(t *testing.T) {
user, err := store.CreateUser(ctx, "testuser2", "password123", "Test User 2")
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
if user.Role != RoleUser {
t.Errorf("Role = %q, want %q (second user should be regular)", user.Role, RoleUser)
}
})
t.Run("bcrypt cost >= 10", func(t *testing.T) {
user, err := store.GetUserByUsername(ctx, "testuser")
if err != nil {
t.Fatalf("GetUserByUsername: %v", err)
}
cost, err := bcrypt.Cost([]byte(user.PasswordHash))
if err != nil {
t.Fatalf("bcrypt.Cost: %v", err)
}
if cost < 10 {
t.Errorf("bcrypt cost = %d, want >= 10", cost)
}
})
}
func TestUserStore_DuplicateUsername(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10)
ctx := context.Background()
if _, err := store.CreateUser(ctx, "dupuser", "password123", ""); err != nil {
t.Fatalf("CreateUser: %v", err)
}
_, err := store.CreateUser(ctx, "dupuser", "password456", "")
if err != ErrDuplicateUsername {
t.Errorf("expected ErrDuplicateUsername, got %v", err)
}
}
func TestUserStore_InvalidUsername(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10)
ctx := context.Background()
tests := []struct {
name string
username string
}{
{"too short", "ab"},
{"has spaces", "hello world"},
{"has special chars", "user@name"},
{"has dash", "user-name"},
{"empty", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := store.CreateUser(ctx, tt.username, "password123", "")
if err != ErrInvalidUsername {
t.Errorf("expected ErrInvalidUsername for %q, got %v", tt.username, err)
}
})
}
}
func TestUserStore_PasswordValidation(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10)
ctx := context.Background()
t.Run("too short", func(t *testing.T) {
_, err := store.CreateUser(ctx, "shortpw", "short", "")
if err != ErrPasswordTooShort {
t.Errorf("expected ErrPasswordTooShort, got %v", err)
}
})
t.Run("too long", func(t *testing.T) {
longPW := make([]byte, 73)
for i := range longPW {
longPW[i] = 'a'
}
_, err := store.CreateUser(ctx, "longpw", string(longPW), "")
if err != ErrPasswordTooLong {
t.Errorf("expected ErrPasswordTooLong, got %v", err)
}
})
}
func TestUserStore_GetByID(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10)
ctx := context.Background()
user, err := store.CreateUser(ctx, "getbyid", "password123", "Get By ID")
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
got, err := store.GetUserByID(ctx, user.ID)
if err != nil {
t.Fatalf("GetUserByID: %v", err)
}
if got.Username != "getbyid" {
t.Errorf("Username = %q, want %q", got.Username, "getbyid")
}
// Non-existent user
_, err = store.GetUserByID(ctx, 99999)
if err != ErrUserNotFound {
t.Errorf("expected ErrUserNotFound, got %v", err)
}
}
func TestUserStore_VerifyPassword(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10)
ctx := context.Background()
store.CreateUser(ctx, "verifypw", "correctpassword", "")
t.Run("correct password", func(t *testing.T) {
user, err := store.VerifyPassword(ctx, "verifypw", "correctpassword")
if err != nil {
t.Fatalf("VerifyPassword: %v", err)
}
if user.Username != "verifypw" {
t.Errorf("Username = %q, want %q", user.Username, "verifypw")
}
})
t.Run("wrong password", func(t *testing.T) {
_, err := store.VerifyPassword(ctx, "verifypw", "wrongpassword")
if err != ErrInvalidPassword {
t.Errorf("expected ErrInvalidPassword, got %v", err)
}
})
t.Run("non-existent user", func(t *testing.T) {
_, err := store.VerifyPassword(ctx, "nonexistent", "password")
if err != ErrInvalidPassword {
t.Errorf("expected ErrInvalidPassword, got %v", err)
}
})
}
func TestUserStore_UpdatePassword(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10)
ctx := context.Background()
user, _ := store.CreateUser(ctx, "updatepw", "oldpassword1", "")
t.Run("successful update", func(t *testing.T) {
err := store.UpdatePassword(ctx, user.ID, "newpassword1")
if err != nil {
t.Fatalf("UpdatePassword: %v", err)
}
// Old password should fail
_, err = store.VerifyPassword(ctx, "updatepw", "oldpassword1")
if err != ErrInvalidPassword {
t.Error("old password should not work after update")
}
// New password should work
_, err = store.VerifyPassword(ctx, "updatepw", "newpassword1")
if err != nil {
t.Errorf("new password should work: %v", err)
}
})
t.Run("too short new password", func(t *testing.T) {
err := store.UpdatePassword(ctx, user.ID, "short")
if err != ErrPasswordTooShort {
t.Errorf("expected ErrPasswordTooShort, got %v", err)
}
})
t.Run("non-existent user", func(t *testing.T) {
err := store.UpdatePassword(ctx, 99999, "password123")
if err != ErrUserNotFound {
t.Errorf("expected ErrUserNotFound, got %v", err)
}
})
}
func TestUserStore_ListUsers(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10)
ctx := context.Background()
for i := 0; i < 3; i++ {
store.CreateUser(ctx, fmt.Sprintf("listuser%d", i), "password123", "")
}
users, err := store.ListUsers(ctx)
if err != nil {
t.Fatalf("ListUsers: %v", err)
}
if len(users) != 3 {
t.Errorf("got %d users, want 3", len(users))
}
}
func TestUserStore_CountUsers(t *testing.T) {
db := newTestDB(t)
store := NewSQLiteUserStore(db, 10)
ctx := context.Background()
count, err := store.CountUsers(ctx)
if err != nil {
t.Fatalf("CountUsers: %v", err)
}
if count != 0 {
t.Errorf("initial count = %d, want 0", count)
}
store.CreateUser(ctx, "countuser", "password123", "")
count, err = store.CountUsers(ctx)
if err != nil {
t.Fatalf("CountUsers: %v", err)
}
if count != 1 {
t.Errorf("count after create = %d, want 1", count)
}
}