Files
code-utils/synccrypto_test.go
2026-08-14 07:52:01 +08:00

194 lines
5.3 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
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)
}
}