后端基础层:模块定义、配置解析、SQLite 存储层、Argon2id 认证与 Markdown 渲染
- internal/config:serve/init/backup/gc 子命令参数解析(--dev 强制 loopback 守卫) - internal/store:user_version 版本化迁移(仅追加式 + 越界拒启 + --allow-newer)、 笔记/图片/引用/会话/设置 DAO、并集可见性查询、VACUUM INTO 在线备份、 回收站 30 天 + 孤儿图 7 天宽限 gc - internal/auth:Argon2id PHC 串(m=19456,t=2,p=1)、256bit token 与 SHA-256 摘要 - internal/markdown:goldmark(默认转义)+ bluemonday 双保险,摘要纯文本提取 - internal/middleware:安全头(CSP/HSTS/nosniff 等)、错误日志、 有界令牌桶限流(per-IP 桶上限 + TTL 逐出)、Origin/Referer 同源校验
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestMigrationsFromEmpty 空库 → 最新版本;重复打开幂等(§13 迁移组)。
|
||||
func TestMigrationsFromEmpty(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dbPath := filepath.Join(dir, "pure-note.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, "pure-note.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, "+")"); 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("物理删除笔记后其引用关系应被级联删除(图片转为孤儿)")
|
||||
}
|
||||
}
|
||||
|
||||
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:])
|
||||
}
|
||||
Reference in New Issue
Block a user