更新若干功能

This commit is contained in:
李琦
2026-08-14 07:52:01 +08:00
parent 153c7ed448
commit 89bc265f68
196 changed files with 17675 additions and 0 deletions

193
synccrypto_test.go Normal file
View File

@@ -0,0 +1,193 @@
package main
import (
"context"
"encoding/hex"
"path/filepath"
"testing"
)
func TestSyncChangePasswordGuards(t *testing.T) {
s, e := OpenStore(filepath.Join(t.TempDir(), "pwd.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
a := NewApp()
a.store = s
a.ctx = context.Background()
if e := a.SyncChangePassword("old-pass", "new-pass-123"); e == nil || e.Error() != "SYNC_NOT_LOGGED_IN" {
t.Fatalf("want SYNC_NOT_LOGGED_IN, got %v", e)
}
if e := s.SetMeta("sync_user_id", "1"); e != nil {
t.Fatal(e)
}
if e := a.SyncChangePassword("old-pass", "123"); e == nil || e.Error() != "SYNC_PASSWORD_TOO_SHORT" {
t.Fatalf("want SYNC_PASSWORD_TOO_SHORT, got %v", e)
}
// 显式指向不可达地址:在线校验必须明确报错而不是悄悄跳过。
for k, v := range map[string]string{"sync_host": "127.0.0.1", "sync_port": "1", "sync_user": "nobody", "sync_database": "none"} {
if e := s.SetMeta(k, v); e != nil {
t.Fatal(e)
}
}
if e := a.SyncChangePassword("old-pass", "new-pass-123"); e == nil || e.Error() != "SYNC_OFFLINE" {
t.Fatalf("want SYNC_OFFLINE, got %v", e)
}
}
func TestSyncStatusPending(t *testing.T) {
s, e := OpenStore(filepath.Join(t.TempDir(), "pending.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
a := NewApp()
a.store = s
st, e := a.GetSyncStatus()
if e != nil || st.Pending != 0 {
t.Fatalf("want pending=0, got %d err=%v", st.Pending, e)
}
// 走正常保存路径,新建即 dirty=1。
for _, title := range []string{"a", "b"} {
if _, e := a.SaveTodo(Todo{Title: title}); e != nil {
t.Fatal(e)
}
}
if st, _ = a.GetSyncStatus(); st.Pending != 2 {
t.Fatalf("want pending=2, got %d", st.Pending)
}
}
func TestSyncConfigDefaults(t *testing.T) {
s, e := OpenStore(filepath.Join(t.TempDir(), "cfg.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
a := NewApp()
a.store = s
c := a.syncConfig()
if c.Host != defaultSyncHost || c.User != defaultSyncUser || c.Password != defaultSyncPassword || c.Database != defaultSyncDatabase || c.Port != 3306 {
t.Fatalf("defaults not applied: %+v", c)
}
// meta 存过任意自定义配置后,以 meta 为准,不再混入默认值。
if e := s.SetMeta("sync_host", "db.example.com"); e != nil {
t.Fatal(e)
}
c = a.syncConfig()
if c.Host != "db.example.com" || c.User != "" || c.Password != "" || c.Database != "" {
t.Fatalf("explicit meta should win: %+v", c)
}
}
func TestEncryptDecryptRoundtrip(t *testing.T) {
saltHex, e := newSaltHex()
if e != nil {
t.Fatal(e)
}
salt, _ := hex.DecodeString(saltHex)
key := deriveEncKey("qiqi991012", salt)
if len(key) != 32 {
t.Fatalf("key length = %d, want 32", len(key))
}
plain := `{"sparkKey":"sk-abc","deepSeekKey":"dsk-测试"}`
enc, e := encryptWithKey(key, plain)
if e != nil {
t.Fatal(e)
}
if enc == plain {
t.Fatal("ciphertext equals plaintext")
}
got, e := decryptWithKey(key, enc)
if e != nil {
t.Fatal(e)
}
if got != plain {
t.Fatalf("roundtrip mismatch: %q", got)
}
}
func TestDecryptRejectsWrongKeyAndTamper(t *testing.T) {
salt := []byte("0123456789abcdef")
key := deriveEncKey("password-a", salt)
enc, e := encryptWithKey(key, "secret")
if e != nil {
t.Fatal(e)
}
if _, e = decryptWithKey(deriveEncKey("password-b", salt), enc); e == nil {
t.Fatal("wrong key should fail decryption")
}
if _, e = decryptWithKey(deriveEncKey("password-a", []byte("fedcba9876543210")), enc); e == nil {
t.Fatal("wrong salt should fail decryption")
}
tampered := enc[:len(enc)-8] + "AAAAAAA="
if _, e = decryptWithKey(key, tampered); e == nil {
t.Fatal("tampered blob should fail decryption")
}
if _, e = decryptWithKey(key, "not-base64!!"); e == nil {
t.Fatal("invalid base64 should fail")
}
}
func TestDeriveEncKeyDeterministic(t *testing.T) {
salt := []byte("0123456789abcdef")
a := deriveEncKey("same-password", salt)
b := deriveEncKey("same-password", salt)
if hex.EncodeToString(a) != hex.EncodeToString(b) {
t.Fatal("same password+salt must derive the same key")
}
c := deriveEncKey("other-password", salt)
if hex.EncodeToString(a) == hex.EncodeToString(c) {
t.Fatal("different passwords must derive different keys")
}
}
func TestSaveSettingsBumpsAPIKeyTimestampOnlyOnKeyChange(t *testing.T) {
s, e := OpenStore(filepath.Join(t.TempDir(), "keys.db"))
if e != nil {
t.Fatal(e)
}
defer s.db.Close()
base, e := s.Settings()
if e != nil {
t.Fatal(e)
}
// 只改主题:不应产生 API Key 时间戳。
base.Theme = "light"
if e = s.SaveSettings(base); e != nil {
t.Fatal(e)
}
if got := s.Meta("api_keys_updated_at"); got != "" {
t.Fatalf("theme-only save should not bump key timestamp, got %q", got)
}
// 改 Key时间戳出现。
base.SparkKey = "sk-123"
if e = s.SaveSettings(base); e != nil {
t.Fatal(e)
}
first := s.Meta("api_keys_updated_at")
if first == "" {
t.Fatal("key change should bump timestamp")
}
// 不改 Key 再存:时间戳不变。
base.GlassOpacity = 60
if e = s.SaveSettings(base); e != nil {
t.Fatal(e)
}
if got := s.Meta("api_keys_updated_at"); got != first {
t.Fatalf("timestamp changed without key change: %q -> %q", first, got)
}
// 开关与 Key 一起持久化。
base.SyncAPIKeys = true
if e = s.SaveSettings(base); e != nil {
t.Fatal(e)
}
got, e := s.Settings()
if e != nil {
t.Fatal(e)
}
if !got.SyncAPIKeys || got.SparkKey != "sk-123" {
t.Fatalf("settings not persisted: %+v", got)
}
}