Files
code-utils/synccrypto_test.go
2026-08-15 07:29:45 +08:00

198 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}