ai-servers/llm-gateway/internal/auth/store_test.go
Ray Andrew 90adf6f3a8
feat(gateway): add circuit breaker, retry, and concurrency limit support
feat(gateway): add debug logging with file storage and retention

feat(gateway): add audit logging for user actions

feat(gateway): add request ID tracking and rate limit headers

feat(gateway): add model aliases and load balancing strategies

feat(gateway): add config hot-reload via SIGHUP

feat(gateway): add CORS support

feat(gateway): add data export API and dashboard endpoints

feat(gateway): add dashboard pages for audit and debug logs

feat(gateway): add concurrent request limiting per token

feat(gateway): add streaming timeout support

feat(gateway): add migration support for new schema fields
2026-02-15 04:21:40 -06:00

300 lines
6.8 KiB
Go

package auth
import (
"database/sql"
"testing"
"time"
_ "modernc.org/sqlite"
)
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("opening test db: %v", err)
}
// Create tables
_, err = db.Exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT DEFAULT '',
password_hash TEXT NOT NULL,
is_admin INTEGER DEFAULT 0,
totp_secret TEXT DEFAULT '',
totp_enabled INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
CREATE TABLE api_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL,
user_id INTEGER NOT NULL,
rate_limit_rpm INTEGER DEFAULT 0,
daily_budget_usd REAL DEFAULT 0,
max_concurrent INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
last_used_at INTEGER DEFAULT 0
);
`)
if err != nil {
t.Fatalf("creating tables: %v", err)
}
return db
}
func TestCreateUser(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
user, err := store.CreateUser("alice", "password123", true)
if err != nil {
t.Fatalf("CreateUser: %v", err)
}
if user.Username != "alice" {
t.Errorf("expected username 'alice', got '%s'", user.Username)
}
if !user.IsAdmin {
t.Error("expected admin user")
}
if user.ID == 0 {
t.Error("expected non-zero ID")
}
}
func TestGetUserByUsername(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
store.CreateUser("bob", "password123", false)
user, err := store.GetUserByUsername("bob")
if err != nil {
t.Fatalf("GetUserByUsername: %v", err)
}
if user.Username != "bob" {
t.Errorf("expected 'bob', got '%s'", user.Username)
}
_, err = store.GetUserByUsername("nonexistent")
if err == nil {
t.Error("expected error for nonexistent user")
}
}
func TestCheckPassword(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
store.CreateUser("charlie", "correctpassword", false)
user, _ := store.GetUserByUsername("charlie")
if !store.CheckPassword(user, "correctpassword") {
t.Error("correct password should match")
}
if store.CheckPassword(user, "wrongpassword") {
t.Error("wrong password should not match")
}
}
func TestUpdatePassword(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
user, _ := store.CreateUser("dave", "oldpass12", false)
if err := store.UpdatePassword(user.ID, "newpass12"); err != nil {
t.Fatalf("UpdatePassword: %v", err)
}
user, _ = store.GetUserByUsername("dave")
if store.CheckPassword(user, "oldpass12") {
t.Error("old password should not work")
}
if !store.CheckPassword(user, "newpass12") {
t.Error("new password should work")
}
}
func TestDeleteUser(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
user1, _ := store.CreateUser("admin1", "password1234", true)
user2, _ := store.CreateUser("user2", "password1234", false)
// Can delete non-admin
if err := store.DeleteUser(user2.ID); err != nil {
t.Fatalf("DeleteUser: %v", err)
}
// Cannot delete last admin
if err := store.DeleteUser(user1.ID); err == nil {
t.Error("should not be able to delete last admin")
}
}
func TestHasAnyUser(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
if store.HasAnyUser() {
t.Error("should have no users initially")
}
store.CreateUser("first", "password1234", true)
if !store.HasAnyUser() {
t.Error("should have users after creation")
}
}
func TestSessionCRUD(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
user, _ := store.CreateUser("sessuser", "password1234", false)
sessionID, err := store.CreateSession(user.ID, 1*time.Hour)
if err != nil {
t.Fatalf("CreateSession: %v", err)
}
sess, err := store.GetSession(sessionID)
if err != nil {
t.Fatalf("GetSession: %v", err)
}
if sess.UserID != user.ID {
t.Errorf("expected user ID %d, got %d", user.ID, sess.UserID)
}
if err := store.DeleteSession(sessionID); err != nil {
t.Fatalf("DeleteSession: %v", err)
}
_, err = store.GetSession(sessionID)
if err == nil {
t.Error("session should be deleted")
}
}
func TestStaticTokenLookup(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
staticTokens := []StaticToken{
{Name: "test-token", Key: "sk-test-key-12345678", RateLimitRPM: 60, DailyBudgetUSD: 10.0, MaxConcurrent: 5},
}
store := NewStore(db, staticTokens)
token, err := store.LookupAPIToken("sk-test-key-12345678")
if err != nil {
t.Fatalf("LookupAPIToken: %v", err)
}
if token.Name != "test-token" {
t.Errorf("expected 'test-token', got '%s'", token.Name)
}
if token.ID != -1 {
t.Errorf("static token should have ID -1, got %d", token.ID)
}
if token.RateLimitRPM != 60 {
t.Errorf("expected RPM 60, got %d", token.RateLimitRPM)
}
if token.MaxConcurrent != 5 {
t.Errorf("expected max_concurrent 5, got %d", token.MaxConcurrent)
}
// Non-existent token
_, err = store.LookupAPIToken("nonexistent")
if err == nil {
t.Error("should error on nonexistent token")
}
}
func TestDBTokenCRUD(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
user, _ := store.CreateUser("tokenuser", "password1234", false)
plainKey, token, err := store.CreateAPIToken(user.ID, "my-token", 100, 5.0)
if err != nil {
t.Fatalf("CreateAPIToken: %v", err)
}
if plainKey == "" {
t.Error("plain key should not be empty")
}
if token.Name != "my-token" {
t.Errorf("expected 'my-token', got '%s'", token.Name)
}
// Lookup by key
found, err := store.LookupAPIToken(plainKey)
if err != nil {
t.Fatalf("LookupAPIToken: %v", err)
}
if found.Name != "my-token" {
t.Errorf("expected 'my-token', got '%s'", found.Name)
}
// List tokens
tokens, err := store.ListAPITokens(user.ID)
if err != nil {
t.Fatalf("ListAPITokens: %v", err)
}
if len(tokens) != 1 {
t.Errorf("expected 1 token, got %d", len(tokens))
}
// Delete
if err := store.DeleteAPIToken(token.ID); err != nil {
t.Fatalf("DeleteAPIToken: %v", err)
}
_, err = store.LookupAPIToken(plainKey)
if err == nil {
t.Error("token should be deleted")
}
}
func TestSetStaticTokens(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
store := NewStore(db, nil)
_, err := store.LookupAPIToken("key1")
if err == nil {
t.Error("should not find token before setting")
}
store.SetStaticTokens([]StaticToken{
{Name: "new-token", Key: "key1"},
})
token, err := store.LookupAPIToken("key1")
if err != nil {
t.Fatalf("after SetStaticTokens: %v", err)
}
if token.Name != "new-token" {
t.Errorf("expected 'new-token', got '%s'", token.Name)
}
}