package store import ( "bytes" "context" "os" "path/filepath" "testing" "time" ) func mustNow() time.Time { return time.Now().UTC().Truncate(time.Millisecond) } // testKey — a deterministic 32-byte key for tests. func testKey(b byte) []byte { k := make([]byte, 32) for i := range k { k[i] = b + byte(i) } return k } func TestEncryptedRoundTrip(t *testing.T) { ctx := context.Background() dir := t.TempDir() cipher := filepath.Join(dir, "maven.db") tmpfs := filepath.Join(dir, "work.db") // tmpfs stand-in for the test key := testKey(1) s, err := OpenEncrypted(ctx, cipher, tmpfs, key) if err != nil { t.Fatalf("OpenEncrypted: %v", err) } if _, err := s.SetValue(ctx, KindSelf, "water", "tap:water", map[string]int{"ml": 250}, mustNow()); err != nil { t.Fatalf("SetValue: %v", err) } if err := s.Close(); err != nil { t.Fatalf("Close: %v", err) } // Ciphertext exists, plaintext working copy is wiped. if _, err := os.Stat(cipher); err != nil { t.Fatalf("ciphertext missing after Close: %v", err) } if _, err := os.Stat(tmpfs); !os.IsNotExist(err) { t.Fatalf("working copy not wiped: %v", err) } // Reopen with same key, read the fact back. s2, err := OpenEncrypted(ctx, cipher, tmpfs, testKey(1)) if err != nil { t.Fatalf("reopen: %v", err) } defer s2.Close() f, err := s2.LatestFact(ctx, "water") if err != nil { t.Fatalf("LatestFact: %v", err) } if f.Key != "water" || f.Source != "tap:water" { t.Fatalf("got %+v", f) } } func TestWrongKeyFailsClosed(t *testing.T) { ctx := context.Background() dir := t.TempDir() cipher := filepath.Join(dir, "maven.db") tmpfs := filepath.Join(dir, "work.db") s, err := OpenEncrypted(ctx, cipher, tmpfs, testKey(1)) if err != nil { t.Fatalf("OpenEncrypted: %v", err) } if _, err := s.SetValue(ctx, KindSelf, "water", "tap:water", 1, mustNow()); err != nil { t.Fatalf("SetValue: %v", err) } if err := s.Close(); err != nil { t.Fatalf("Close: %v", err) } s2, err := OpenEncrypted(ctx, cipher, tmpfs, testKey(9)) // wrong key if err == nil { s2.Close() t.Fatal("expected wrong key to fail, got nil error") } // Must not have left a decrypted working copy behind. if _, statErr := os.Stat(tmpfs); !os.IsNotExist(statErr) { t.Fatalf("wrong-key open leaked a working copy: %v", statErr) } } func TestFirstRunUpgrade(t *testing.T) { ctx := context.Background() dir := t.TempDir() cipher := filepath.Join(dir, "maven.db") tmpfs := filepath.Join(dir, "work.db") // Seed a PLAINTEXT db at the on-disk path (legacy state). plain, err := Open(ctx, cipher) if err != nil { t.Fatalf("seed Open: %v", err) } if _, err := plain.SetValue(ctx, KindSelf, "water", "tap:water", 42, mustNow()); err != nil { t.Fatalf("seed SetValue: %v", err) } if err := plain.Close(); err != nil { t.Fatalf("seed Close: %v", err) } if isCiphertext(mustRead(t, cipher)) { t.Fatal("seed db should be plaintext") } // Open encrypted: upgrades in place. key := testKey(3) s, err := OpenEncrypted(ctx, cipher, tmpfs, key) if err != nil { t.Fatalf("OpenEncrypted upgrade: %v", err) } f, err := s.LatestFact(ctx, "water") if err != nil { t.Fatalf("LatestFact after upgrade: %v", err) } if f.Key != "water" { t.Fatalf("upgraded data wrong: %+v", f) } if err := s.Close(); err != nil { t.Fatalf("Close: %v", err) } // On-disk file is now ciphertext, not a readable sqlite db. if !isCiphertext(mustRead(t, cipher)) { t.Fatal("on-disk file still plaintext after upgrade") } if _, err := Open(ctx, cipher); err == nil { t.Fatal("ciphertext opened as plaintext sqlite — should fail") } } func TestNoPlaintextValueOnDisk(t *testing.T) { ctx := context.Background() dir := t.TempDir() cipher := filepath.Join(dir, "maven.db") tmpfs := filepath.Join(dir, "work.db") const secret = "SUPERSECRET_MARKER_VALUE_12345" s, err := OpenEncrypted(ctx, cipher, tmpfs, testKey(7)) if err != nil { t.Fatalf("OpenEncrypted: %v", err) } if _, err := s.SetValue(ctx, KindSelf, "note", "tap:note", secret, mustNow()); err != nil { t.Fatalf("SetValue: %v", err) } if err := s.Close(); err != nil { t.Fatalf("Close: %v", err) } if bytes.Contains(mustRead(t, cipher), []byte(secret)) { t.Fatal("plaintext secret found in ciphertext file") } } func mustRead(t *testing.T, p string) []byte { t.Helper() b, err := os.ReadFile(p) if err != nil { t.Fatalf("read %s: %v", p, err) } return b }