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