// 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") }