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 pkg := packagedSyncDefaults() a.applyPackagedSyncConfig() c := a.syncConfig() if c.Host != pkg.Host || c.User != pkg.User || c.Password != pkg.Password || c.Database != pkg.Database || c.Port != pkg.Port { t.Fatalf("want packaged defaults written to sqlite: %+v got %+v", pkg, c) } // 启动覆盖后,本地残留的旧地址会被打包配置盖掉。 _ = s.SetMeta("sync_host", "127.0.0.1") _ = s.SetMeta("sync_user", "root") _ = s.SetMeta("sync_password", "root") _ = s.SetMeta("sync_database", "code_count") a.applyPackagedSyncConfig() c = a.syncConfig() if c.Host != pkg.Host || c.User != pkg.User { t.Fatalf("packaged config must overwrite sqlite meta: %+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) } }