Files
pure-note/internal/store/store_test.go
T
wangairnan 5457aeaa33 feat: 新增发布日期自选、站点 Logo、回收站清空等十项改进
- 编辑页标题改 filled 变体;标签改 tags 选择器(可勾选既有/输入新建)
- 笔记发布日期可自选(schema v2 新增 published_at 并回填),前台展示发布日期
- 登录页移除「请输入管理密码以继续」「连续失败将被暂时锁定」文案
- 站点设置每页条数收敛为 10/20/30/50 选择器,后端白名单同源校验
- 站点设置两卡片宽屏左右/窄屏上下;修改密码按钮改常规大小
- 后台各页顶栏固定高度,切换页面不再抖动
- 站点 Logo:settings 新增 site_logo(站内路径),gc/孤儿清单豁免 Logo 图片,
  博客 header 站点名前展示,设置页支持上传/更换/清除
- 回收站右上角一键清空(DELETE /api/admin/trash,含确认弹窗)
- pn init 写入 Markdown 语法示例文档(slug welcome,公开)与程序生成的示例图片
2026-09-09 11:10:30 +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:])
}