Files
pure-note/internal/store/store_test.go
T
wangairnan 132c4a1ba2 fix(store): 修正邻接查询、GC 竞态、备份权限与设置解析,新增 OpenData 纯数据打开
邻接查询 tie-break 与 pinned 排序键与列表序对齐,补表驱动测试(round2 P1-1); GC DELETE 带条件复查,防 SELECT 与 DELETE 间恢复/引用竞态(P2-6); 备份经 umask 收紧创建即 0600,消除 chmod 前暴露窗口(P2-7); page_size 改 strconv.Atoi 全文解析,拒部分解析脏值(P2-13); OpenData 不迁移、不做版本守卫(D6/P1-5)。
2026-09-08 17:32:47 +08:00

215 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 store
import (
"errors"
"path/filepath"
"testing"
)
// TestMigrationsFromEmpty 空库 → 最新版本;重复打开幂等(§13 迁移组)。
func TestMigrationsFromEmpty(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "pn.db")
s1, err := Open(dbPath, false)
if err != nil {
t.Fatalf("首次打开失败: %v", err)
}
v1, err := s1.SchemaVersion()
if err != nil {
t.Fatal(err)
}
if v1 != MaxSchemaVersion {
t.Fatalf("期望版本 %d 实际 %d", MaxSchemaVersion, v1)
}
s1.Close()
// 重开:幂等,不再执行迁移
s2, err := Open(dbPath, false)
if err != nil {
t.Fatalf("二次打开失败: %v", err)
}
defer s2.Close()
v2, err := s2.SchemaVersion()
if err != nil {
t.Fatal(err)
}
if v2 != v1 {
t.Fatalf("迁移不幂等: %d → %d", v1, v2)
}
}
// TestMigrationNewerRejected user_version 越界拒绝启动;--allow-newer 放行(§10.4)。
func TestMigrationNewerRejected(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "pn.db")
s1, err := Open(dbPath, false)
if err != nil {
t.Fatal(err)
}
if _, err := s1.DB().Exec("PRAGMA user_version = 99"); err != nil {
t.Fatal(err)
}
s1.Close()
_, err = Open(dbPath, false)
if !errors.Is(err, ErrSchemaNewer) {
t.Fatalf("期望 ErrSchemaNewer,实际 %v", err)
}
s2, err := Open(dbPath, true) // --allow-newer
if err != nil {
t.Fatalf("allow-newer 应放行: %v", err)
}
defer s2.Close()
}
// TestForeignKeysCascade 外键级联生效(image_refs 依赖 foreign_keys=1)。
func TestForeignKeysCascade(t *testing.T) {
s := openTestStore(t)
nid, err := s.CreateNote(&Note{Slug: "casc", Title: "级联", Status: "public",
Tags: []string{}, CreatedAt: 1, UpdatedAt: 1})
if err != nil {
t.Fatal(err)
}
imgID, err := s.UpsertImage("aa", "image/png", 1, []byte{1}, 1)
if err != nil {
t.Fatal(err)
}
if err := s.RebuildImageRefs(nid, "![x](/api/images/"+itoa64(imgID)+")"); err != nil {
t.Fatal(err)
}
if err := s.DeleteNoteForever(nid); err != nil {
t.Fatal(err)
}
// 级联后图片变孤儿
orphans, err := s.ListOrphanImages()
if err != nil {
t.Fatal(err)
}
found := false
for _, o := range orphans {
if o.ID == imgID {
found = true
}
}
if !found {
t.Fatal("物理删除笔记后其引用关系应被级联删除(图片转为孤儿)")
}
}
// TestOpenDataNoMigrate 维护命令打开方式:不迁移、不做版本守卫(D6,评审 round2 P1-5)。
func TestOpenDataNoMigrate(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "pn.db")
// 全新路径:OpenData 不创建 schema
s, err := OpenData(dbPath)
if err != nil {
t.Fatal(err)
}
v, err := s.SchemaVersion()
if err != nil {
t.Fatal(err)
}
if v != 0 {
t.Fatalf("OpenData 不应执行迁移,user_version=%d", v)
}
var one int
if err := s.db.QueryRow("SELECT 1 FROM notes LIMIT 1").Scan(&one); err == nil {
t.Fatal("OpenData 不应创建表")
}
s.Close()
// 更高 schema 版本:OpenData 不被守卫拒绝(升级/回滚场景备份仍可用)
s2, err := Open(dbPath, false)
if err != nil {
t.Fatal(err)
}
if _, err := s2.DB().Exec("PRAGMA user_version = 99"); err != nil {
t.Fatal(err)
}
s2.Close()
if _, err := Open(dbPath, false); !errors.Is(err, ErrSchemaNewer) {
t.Fatalf("Open 仍应被守卫拒绝,实际 %v", err)
}
s3, err := OpenData(dbPath)
if err != nil {
t.Fatalf("OpenData 不做版本守卫,应放行: %v", err)
}
s3.Close()
}
// TestAdjacentPublicNote 邻接查询:列表序 pinned DESC, updated_at DESC, id DESC
// 的紧邻语义(评审 round2 P1-1:同秒 tie-break 与 pinned 参与排序;仅统计公开)。
func TestAdjacentPublicNote(t *testing.T) {
s := openTestStore(t)
mk := func(slug string, at int64, pinned bool, status string) {
t.Helper()
if _, err := s.CreateNote(&Note{Slug: slug, Title: slug, Status: status,
Tags: []string{}, Pinned: pinned, CreatedAt: at, UpdatedAt: at}); err != nil {
t.Fatal(err)
}
}
mk("a", 100, false, "public") // id=1
mk("b", 100, false, "public") // id=2(与 a 同秒,列表中排在 a 之前)
mk("c", 80, false, "public") // id=3
mk("p", 50, true, "public") // id=4 置顶 → 列表首
mk("priv", 200, false, "private") // 更新的私有笔记:不得出现在邻接中
get := func(slug string) *Note {
n, err := s.GetNoteBySlug(slug)
if err != nil {
t.Fatal(err)
}
return n
}
// 公开列表序:p > b > a > c(priv 被过滤)
cases := []struct{ slug, prev, next string }{
{"p", "", "b"}, // 置顶首篇
{"b", "p", "a"},
{"a", "b", "c"}, // a 与 b 同秒:prev 应取同秒中紧邻的 b(id 更大),而非跳过
{"c", "a", ""},
}
for _, tc := range cases {
prevSlug, _, err := s.AdjacentPublicNote(get(tc.slug), "prev")
if err != nil {
t.Fatalf("%s prev: %v", tc.slug, err)
}
nextSlug, _, err := s.AdjacentPublicNote(get(tc.slug), "next")
if err != nil {
t.Fatalf("%s next: %v", tc.slug, err)
}
if prevSlug != tc.prev || nextSlug != tc.next {
t.Errorf("%s: 期望 prev=%q next=%q,实际 prev=%q next=%q",
tc.slug, tc.prev, tc.next, prevSlug, nextSlug)
}
}
if _, _, err := s.AdjacentPublicNote(get("a"), "bad"); err == nil {
t.Error("非法 dir 应返回错误")
}
}
func openTestStore(t *testing.T) *Store {
t.Helper()
s, err := Open(filepath.Join(t.TempDir(), "test.db"), false)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { s.Close() })
return s
}
func itoa64(v int64) string {
if v == 0 {
return "0"
}
var b [20]byte
i := len(b)
for v > 0 {
i--
b[i] = byte('0' + v%10)
v /= 10
}
return string(b[i:])
}