feat: add SQLite storage for login attempts and sessions
Adds persistent storage using modernc.org/sqlite (pure Go). Login attempts are deduplicated by (username, password, ip) with counts. Sessions and session logs are tracked with UUID IDs. Includes embedded SQL migrations, configurable retention with background pruning, and an in-memory store for tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
83
internal/storage/migrations_test.go
Normal file
83
internal/storage/migrations_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestMigrateCreatesTablesAndVersion(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := Migrate(db); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
// Verify schema version.
|
||||
var version int
|
||||
if err := db.QueryRow(`SELECT version FROM schema_version`).Scan(&version); err != nil {
|
||||
t.Fatalf("query version: %v", err)
|
||||
}
|
||||
if version != 1 {
|
||||
t.Errorf("version = %d, want 1", version)
|
||||
}
|
||||
|
||||
// Verify tables exist by inserting into them.
|
||||
_, err = db.Exec(`INSERT INTO login_attempts (username, password, ip, count, first_seen, last_seen) VALUES ('a', 'b', 'c', 1, '2024-01-01', '2024-01-01')`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert into login_attempts: %v", err)
|
||||
}
|
||||
_, err = db.Exec(`INSERT INTO sessions (id, ip, username, shell_name, connected_at) VALUES ('test-id', 'c', 'a', '', '2024-01-01')`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert into sessions: %v", err)
|
||||
}
|
||||
_, err = db.Exec(`INSERT INTO session_logs (session_id, timestamp, input, output) VALUES ('test-id', '2024-01-01', '', '')`)
|
||||
if err != nil {
|
||||
t.Fatalf("insert into session_logs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateIdempotent(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Run twice; second should be a no-op.
|
||||
if err := Migrate(db); err != nil {
|
||||
t.Fatalf("first migrate: %v", err)
|
||||
}
|
||||
if err := Migrate(db); err != nil {
|
||||
t.Fatalf("second migrate: %v", err)
|
||||
}
|
||||
|
||||
var version int
|
||||
if err := db.QueryRow(`SELECT version FROM schema_version`).Scan(&version); err != nil {
|
||||
t.Fatalf("query version: %v", err)
|
||||
}
|
||||
if version != 1 {
|
||||
t.Errorf("version = %d after double migrate, want 1", version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMigrations(t *testing.T) {
|
||||
migrations, err := loadMigrations()
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
if len(migrations) == 0 {
|
||||
t.Fatal("no migrations found")
|
||||
}
|
||||
if migrations[0].Version != 1 {
|
||||
t.Errorf("first migration version = %d, want 1", migrations[0].Version)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user