Files
tamabox/internal/db/vote_options_test.go
tamakyi 5fdc3df22d 修复: 评论组件泄露私密 token、cwebp 无超时、选项长度与去重
Waline(安全)
- item.html 的 path 从 window.location.href 改为剔除 ?t= 后的 URL。
  私密题 /_/<domain>/<id>?t=<token> 的钥匙原本会整条发往 Waline 服务端
  并落进其访问日志。只删 t、保留其他参数,公开题的评论线程键不变。

cwebp(可靠性)
- ConvertGifToAnimatedWebP 改用 exec.CommandContext + 30s 上限。
  原来裸 exec.Command 无超时,畸形/超大 GIF 会把子进程和上传 goroutine 一起挂住。
  超时杀进程并记 Warn,调用方按既有逻辑回退为原 GIF,不影响上传成功。

投票选项长度(正确性)
- vote_options.content 由 varchar(200) 放宽到 300,与配置默认值一致。
  此前 201–300 字的选项通过校验后在写库阶段失败。
- 新增 db.MaxVoteOptionLen;voteOptionMaxLen() 取配置与存储上限的较小值,
  校验与前端 maxlength 同源,避免配置调大后再次越界。

多选 checkbox 去重(正确性)
- 抽出 appendUniqueValue,预置值与「其他」文本共用同一 seen 集合。
  此前重复提交同名字段会让同一选项在统计里被计多次。

测试
- vote_dedupe_test.go:去重与顺序。
- vote_option_len_test.go:配置小于/等于/大于存储上限三种情形。
- TestVoteOptionMaxLengthRoundTrip:300 个多字节字符存回不截断。
- TestVoteOptionColumnWidens:把列改回 200 后 AutoMigrate 能扩回 300(老库升级路径)。

验证:WSL2 Debian + PostgreSQL 全量 go test ./... 27 个包全绿;
隔离实例渲染单题页确认新的 path 逻辑。
2026-11-05 17:04:47 +08:00

206 lines
6.1 KiB
Go
Raw Permalink 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.
// Copyright 2026 tamakyi. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package db
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestVoteOptions(t *testing.T) {
t.Parallel()
db, cleanup := newTestDB(t)
ctx := context.Background()
store := NewVoteOptionsStore(db)
for _, tc := range []struct {
name string
test func(t *testing.T, ctx context.Context, db *voteOptions)
}{
{"Create", testVoteOptionsCreate},
{"BulkCreate", testVoteOptionsBulkCreate},
{"ListByVoteID", testVoteOptionsListByVoteID},
{"DeleteByID", testVoteOptionsDeleteByID},
{"DeleteByVoteID", testVoteOptionsDeleteByVoteID},
{"CountByVoteID", testVoteOptionsCountByVoteID},
} {
t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() {
if err := cleanup("vote_options"); err != nil {
t.Fatal(err)
}
})
tc.test(t, ctx, store.(*voteOptions))
})
}
}
func testVoteOptionsCreate(t *testing.T, ctx context.Context, db *voteOptions) {
got, err := db.Create(ctx, CreateVoteOptionOptions{
VoteID: 1,
Content: "选项 A",
SortOrder: 0,
})
require.Nil(t, err)
require.Equal(t, uint(1), got.ID)
require.Equal(t, "选项 A", got.Content)
}
func testVoteOptionsBulkCreate(t *testing.T, ctx context.Context, db *voteOptions) {
got, err := db.BulkCreate(ctx, 1, []string{"A", "B", "C"})
require.Nil(t, err)
require.Len(t, got, 3)
// SortOrder 应按传入顺序从 0 递增
require.Equal(t, 0, got[0].SortOrder)
require.Equal(t, 2, got[2].SortOrder)
require.Equal(t, "C", got[2].Content)
}
func testVoteOptionsListByVoteID(t *testing.T, ctx context.Context, db *voteOptions) {
_, err := db.BulkCreate(ctx, 1, []string{"B", "A", "C"})
require.Nil(t, err)
_, err = db.BulkCreate(ctx, 2, []string{"X"})
require.Nil(t, err)
got, err := db.ListByVoteID(ctx, 1)
require.Nil(t, err)
require.Len(t, got, 3)
require.Equal(t, "B", got[0].Content, "按 SortOrder 升序:B(sort 0), A(sort 1), C(sort 2)")
require.Equal(t, "C", got[2].Content)
}
func testVoteOptionsDeleteByID(t *testing.T, ctx context.Context, db *voteOptions) {
_, err := db.Create(ctx, CreateVoteOptionOptions{VoteID: 1, Content: "A"})
require.Nil(t, err)
t.Run("normal", func(t *testing.T) {
require.Nil(t, db.DeleteByID(ctx, 1))
got, err := db.ListByVoteID(ctx, 1)
require.Nil(t, err)
require.Len(t, got, 0)
})
t.Run("not found", func(t *testing.T) {
require.Equal(t, ErrVoteOptionNotExist, db.DeleteByID(ctx, 404))
})
}
func testVoteOptionsDeleteByVoteID(t *testing.T, ctx context.Context, db *voteOptions) {
_, err := db.BulkCreate(ctx, 1, []string{"A", "B", "C"})
require.Nil(t, err)
require.Nil(t, db.DeleteByVoteID(ctx, 1))
got, err := db.ListByVoteID(ctx, 1)
require.Nil(t, err)
require.Len(t, got, 0)
}
func testVoteOptionsCountByVoteID(t *testing.T, ctx context.Context, db *voteOptions) {
_, err := db.BulkCreate(ctx, 1, []string{"A", "B"})
require.Nil(t, err)
got, err := db.CountByVoteID(ctx, 1)
require.Nil(t, err)
require.Equal(t, int64(2), got)
got, err = db.CountByVoteID(ctx, 404)
require.Nil(t, err)
require.Equal(t, int64(0), got)
}
// TestVoteOptionMaxLengthRoundTrip 保证「配置允许的长度」真的能存下。
//
// 曾经 conf 允许 300 字而列宽只有 200:201–300 字的选项通过校验后在写库时才失败
// (PostgreSQL 报 value too long,MySQL 严格模式同样报错),用户看到的是「内部错误」。
func TestVoteOptionMaxLengthRoundTrip(t *testing.T) {
conn, cleanup := newTestDB(t)
t.Cleanup(func() {
if err := cleanup("vote_options"); err != nil {
t.Fatal(err)
}
})
ctx := context.Background()
store := NewVoteOptionsStore(conn)
// 结构体标签不能引用常量,列宽与 MaxVoteOptionLen 是两处独立字面量,
// 这里显式比一次,防止只改了一边。
columns, err := conn.Migrator().ColumnTypes(&VoteOption{})
require.Nil(t, err)
for _, ct := range columns {
if ct.Name() != "content" {
continue
}
if length, ok := ct.Length(); ok {
require.EqualValues(t, MaxVoteOptionLen, length, "vote_options.content 列宽必须与 MaxVoteOptionLen 一致")
}
}
// 用多字节字符填满上限,顺便验证按字符而非字节计数。
content := ""
for len([]rune(content)) < MaxVoteOptionLen {
content += "選"
}
option, err := store.Create(ctx, CreateVoteOptionOptions{VoteID: 1, Content: content})
require.Nil(t, err)
got, err := store.ListByVoteID(ctx, 1)
require.Nil(t, err)
require.Len(t, got, 1)
require.Equal(t, option.ID, got[0].ID)
require.Equal(t, MaxVoteOptionLen, len([]rune(got[0].Content)), "存进去的字符数应与上限一致,未被截断")
}
// TestVoteOptionColumnWidens 验证老库升级路径:已经存在 varchar(200) 的列,
// 跑一次 AutoMigrate 后必须变成 MaxVoteOptionLen。
//
// 只改 model 标签是不够的 —— 如果 AutoMigrate 不扩列,老部署上线后
// 201–300 字的选项依旧写不进去,而新库(测试库)永远发现不了这个问题。
func TestVoteOptionColumnWidens(t *testing.T) {
conn, cleanup := newTestDB(t)
t.Cleanup(func() {
if err := cleanup("vote_options"); err != nil {
t.Fatal(err)
}
})
// 退回老库的列宽(两种方言语法不同)。
var alter string
switch conn.Dialector.Name() {
case "postgres":
alter = "ALTER TABLE vote_options ALTER COLUMN content TYPE varchar(200)"
case "mysql":
alter = "ALTER TABLE vote_options MODIFY COLUMN content varchar(200)"
default:
t.Skipf("未适配的方言:%s", conn.Dialector.Name())
}
require.Nil(t, conn.Exec(alter).Error)
contentLength := func() int64 {
columns, err := conn.Migrator().ColumnTypes(&VoteOption{})
require.Nil(t, err)
for _, ct := range columns {
if ct.Name() != "content" {
continue
}
length, ok := ct.Length()
require.True(t, ok, "content 列应有明确长度")
return length
}
t.Fatal("未找到 content 列")
return 0
}
require.EqualValues(t, 200, contentLength(), "前置条件:列宽应已被改回 200")
require.Nil(t, conn.AutoMigrate(&VoteOption{}))
require.EqualValues(t, MaxVoteOptionLen, contentLength(), "AutoMigrate 必须把列宽扩到 MaxVoteOptionLen")
}