package services import ( "cmbone/internal/models" "database/sql" "errors" "testing" ) func newTestStore(t *testing.T) *SuiStore { t.Helper() db, err := sql.Open("sqlite", ":memory:") if err != nil { t.Fatalf("open sqlite: %v", err) } t.Cleanup(func() { _ = db.Close() }) if err := Migrate(db); err != nil { t.Fatalf("migrate: %v", err) } return &SuiStore{DB: db} } func TestMigrateCreatesDefaultDataOnce(t *testing.T) { store := newTestStore(t) if err := Migrate(store.DB); err != nil { t.Fatalf("second migrate: %v", err) } var hotkeyCount int if err := store.DB.QueryRow("SELECT COUNT(*) FROM hotkeys").Scan(&hotkeyCount); err != nil { t.Fatalf("count hotkeys: %v", err) } if hotkeyCount != 2 { t.Fatalf("hotkey count = %d, want 2", hotkeyCount) } var lang string if err := store.DB.QueryRow("SELECT value FROM appconfig WHERE key = 'language'").Scan(&lang); err != nil { t.Fatalf("query language: %v", err) } if lang != "zh" { t.Fatalf("language = %q, want zh", lang) } } func TestAppConfigServiceLanguage(t *testing.T) { store := newTestStore(t) service := NewAppConfigService(store) if got := service.GetLanguage(); got != "zh" { t.Fatalf("default language = %q, want zh", got) } if err := service.SetLanguage("en"); err != nil { t.Fatalf("set language: %v", err) } if got := service.GetLanguage(); got != "en" { t.Fatalf("updated language = %q, want en", got) } } func TestAppConfigServiceSettingsSchema(t *testing.T) { store := newTestStore(t) service := NewAppConfigService(store) groups, err := service.GetSettingsSchema() if err != nil { t.Fatalf("get settings schema: %v", err) } requiredKeys := map[string]string{ "theme.mode": "light", "language": "zh", "window.start_state": "normal", "window.width": "1280", "window.height": "800", "window.remember_state": "true", "shortcut.open_second_window": "Alt+P", "shortcut.open_main_window": "Alt+M", "data.root_mode": "app_config_dir", "data.custom_directory": "", "logs.level": "info", "logs.audit_retention_days": "180", "logs.app_retention_days": "30", } got := make(map[string]models.AppSettingDefinition) for _, group := range groups { for _, item := range group.Items { got[item.Key] = item } } for key, wantDefault := range requiredKeys { item, ok := got[key] if !ok { t.Fatalf("setting %q missing from schema", key) } if item.DefaultValue != wantDefault { t.Fatalf("setting %q default = %q, want %q", key, item.DefaultValue, wantDefault) } if item.Value != wantDefault { t.Fatalf("setting %q value = %q, want %q", key, item.Value, wantDefault) } if item.Options == nil { t.Fatalf("setting %q options is nil, want empty slice", key) } } } func TestMigrateUpgradesWindowRememberStateOldDefault(t *testing.T) { store := newTestStore(t) if _, err := store.DB.Exec(` UPDATE appconfig SET value = 'false', description = '保存主窗口尺寸和最大化状态,T-015 使用该配置。' WHERE key = 'window.remember_state' `); err != nil { t.Fatalf("set old window remember default: %v", err) } if err := Migrate(store.DB); err != nil { t.Fatalf("migrate old window remember default: %v", err) } var value string if err := store.DB.QueryRow("SELECT value FROM appconfig WHERE key = 'window.remember_state'").Scan(&value); err != nil { t.Fatalf("query window remember state: %v", err) } if value != "true" { t.Fatalf("window.remember_state = %q, want true", value) } } func TestAppConfigServiceValidatesSchemaValues(t *testing.T) { store := newTestStore(t) service := NewAppConfigService(store) if err := service.SetAppConfig("theme.mode", "dark"); err != nil { t.Fatalf("set valid theme: %v", err) } gotTheme, err := service.GetAppConfig("theme.mode") if err != nil { t.Fatalf("get theme: %v", err) } if gotTheme != "dark" { t.Fatalf("theme.mode = %q, want dark", gotTheme) } var gotType string if err := store.DB.QueryRow("SELECT type FROM appconfig WHERE key = 'theme.mode'").Scan(&gotType); err != nil { t.Fatalf("get theme type: %v", err) } if gotType != settingTypeSelect { t.Fatalf("theme.mode type = %q, want %q", gotType, settingTypeSelect) } if err := service.SetAppConfig("theme.mode", "purple"); err == nil { t.Fatal("set invalid theme returned nil error") } if err := service.SetAppConfig("window.remember_state", "yes"); err == nil { t.Fatal("set invalid boolean returned nil error") } if err := service.SetAppConfig("window.width", "wide"); err == nil { t.Fatal("set invalid number returned nil error") } if err := service.SetAppConfig("shortcut.open_second_window", "Alt+O"); err == nil { t.Fatal("set readonly shortcut returned nil error") } } func TestAuthServiceLocalLoginLifecycle(t *testing.T) { store := newTestStore(t) service := NewAuthService(store) session, err := service.Login("local", "admin", "admin123") if err != nil { t.Fatalf("login: %v", err) } if !session.Active { t.Fatal("session is not active") } if session.Username != "admin" { t.Fatalf("username = %q, want admin", session.Username) } if session.Provider != "local" { t.Fatalf("provider = %q, want local", session.Provider) } if session.Token == "" { t.Fatal("session token is empty") } current, err := service.GetCurrentSession() if err != nil { t.Fatalf("get current session: %v", err) } if current.ID != session.ID { t.Fatalf("current session id = %d, want %d", current.ID, session.ID) } var loginAuditCount int if err := store.DB.QueryRow("SELECT COUNT(*) FROM audit_logs WHERE action = 'auth.login.success' AND actor = 'admin'").Scan(&loginAuditCount); err != nil { t.Fatalf("count login audit logs: %v", err) } if loginAuditCount != 1 { t.Fatalf("login audit count = %d, want 1", loginAuditCount) } if err := service.Logout(); err != nil { t.Fatalf("logout: %v", err) } current, err = service.GetCurrentSession() if err != nil { t.Fatalf("get current session after logout: %v", err) } if current.Active { t.Fatal("current session is active after logout") } var logoutAuditCount int if err := store.DB.QueryRow("SELECT COUNT(*) FROM audit_logs WHERE action = 'auth.logout' AND actor = 'admin'").Scan(&logoutAuditCount); err != nil { t.Fatalf("count logout audit logs: %v", err) } if logoutAuditCount != 1 { t.Fatalf("logout audit count = %d, want 1", logoutAuditCount) } } func TestAuthServiceLocalLoginFailureWritesAuditLog(t *testing.T) { store := newTestStore(t) service := NewAuthService(store) _, err := service.Login("local", "admin", "bad-password") if !errors.Is(err, errInvalidCredentials) { t.Fatalf("login error = %v, want invalid credentials", err) } var auditCount int if err := store.DB.QueryRow("SELECT COUNT(*) FROM audit_logs WHERE action = 'auth.login.failure' AND actor = 'admin'").Scan(&auditCount); err != nil { t.Fatalf("count audit logs: %v", err) } if auditCount != 1 { t.Fatalf("audit count = %d, want 1", auditCount) } } func TestAuthServiceRemoteProviderReserved(t *testing.T) { store := newTestStore(t) service := NewAuthService(store) _, err := service.Login("remote", "admin", "admin123") if !errors.Is(err, errRemoteProviderNotReady) { t.Fatalf("login error = %v, want remote provider not ready", err) } var auditCount int if err := store.DB.QueryRow("SELECT COUNT(*) FROM audit_logs WHERE action = 'auth.login.failure' AND target_id = 'remote'").Scan(&auditCount); err != nil { t.Fatalf("count audit logs: %v", err) } if auditCount != 1 { t.Fatalf("audit count = %d, want 1", auditCount) } }