// 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" "github.com/tamakyi/TamaBox/internal/dbutil" ) func TestVotes(t *testing.T) { t.Parallel() db, cleanup := newTestDB(t) ctx := context.Background() store := NewVotesStore(db) for _, tc := range []struct { name string test func(t *testing.T, ctx context.Context, db *votes) }{ {"Create", testVotesCreate}, {"GetByID", testVotesGetByID}, {"GetByToken", testVotesGetByToken}, {"ListByUserID", testVotesListByUserID}, {"Update", testVotesUpdate}, {"UpdateStatus", testVotesUpdateStatus}, {"DeleteByID", testVotesDeleteByID}, {"CountByUserID", testVotesCountByUserID}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { if err := cleanup("votes"); err != nil { t.Fatal(err) } }) tc.test(t, ctx, store.(*votes)) }) } } func testVotesCreate(t *testing.T, ctx context.Context, db *votes) { got, err := db.Create(ctx, CreateVoteOptions{ UserID: 1, Title: "今天穿什么?", Description: "单选题", Type: VoteTypeSingle, Visibility: VoteVisibilityPublic, VoterScope: VoteVoterScopeAnyone, ShowResult: VoteShowResultAfterVote, MaxChoices: 1, }) require.Nil(t, err) require.Equal(t, uint(1), got.ID) require.NotEmpty(t, got.LinkToken, "LinkToken 应自动生成") require.Equal(t, VoteStatusDraft, got.Status, "默认状态应为草稿") // RequireCaptcha 不再被 gorm:"default:true" 隐式默认(零值陷阱已修复):Create 未显式传入时按 false 落库。 // 业务层在创建/编辑投票时始终强制 requireCaptcha=true(决策 #3),真实投票行为不受影响。 require.False(t, got.RequireCaptcha, "底层 Create 不再隐式默认开启验证码") } func testVotesGetByID(t *testing.T, ctx context.Context, db *votes) { _, err := db.Create(ctx, CreateVoteOptions{ UserID: 1, Title: "Title - 1", }) require.Nil(t, err) t.Run("normal", func(t *testing.T) { got, err := db.GetByID(ctx, 1) require.Nil(t, err) require.Equal(t, "Title - 1", got.Title) require.Equal(t, uint(1), got.UserID) }) t.Run("not found", func(t *testing.T) { _, err := db.GetByID(ctx, 404) require.Equal(t, ErrVoteNotExist, err) }) } func testVotesGetByToken(t *testing.T, ctx context.Context, db *votes) { created, err := db.Create(ctx, CreateVoteOptions{ UserID: 1, Title: "Title - 1", }) require.Nil(t, err) t.Run("normal", func(t *testing.T) { got, err := db.GetByToken(ctx, created.LinkToken) require.Nil(t, err) require.Equal(t, created.ID, got.ID) }) t.Run("not found", func(t *testing.T) { _, err := db.GetByToken(ctx, "no-such-token") require.Equal(t, ErrVoteNotExist, err) }) } func testVotesListByUserID(t *testing.T, ctx context.Context, db *votes) { for i := 0; i < 3; i++ { _, err := db.Create(ctx, CreateVoteOptions{ UserID: 1, Title: "Vote - 1", }) require.Nil(t, err) } _, err := db.Create(ctx, CreateVoteOptions{ UserID: 2, Title: "Vote - 2", }) require.Nil(t, err) t.Run("all", func(t *testing.T) { got, err := db.ListByUserID(ctx, 1, ListVotesOptions{}) require.Nil(t, err) require.Len(t, got, 3) }) t.Run("status filter", func(t *testing.T) { err := db.UpdateStatus(ctx, 1, VoteStatusPublished) require.Nil(t, err) got, err := db.ListByUserID(ctx, 1, ListVotesOptions{Status: VoteStatusPublished}) require.Nil(t, err) require.Len(t, got, 1) require.Equal(t, uint(1), got[0].ID) }) t.Run("cursor", func(t *testing.T) { got, err := db.ListByUserID(ctx, 1, ListVotesOptions{ Cursor: &dbutil.Cursor{Value: 3, PageSize: 10}, }) require.Nil(t, err) require.Len(t, got, 2) }) } func testVotesUpdate(t *testing.T, ctx context.Context, db *votes) { _, err := db.Create(ctx, CreateVoteOptions{ UserID: 1, Title: "Title - 1", }) require.Nil(t, err) t.Run("normal", func(t *testing.T) { vt := VoteTypeMultiple mc := 3 err := db.Update(ctx, 1, UpdateVoteOptions{ Title: "Title - updated", Description: "desc", Type: &vt, Visibility: VoteVisibilityLinkOnly, VoterScope: VoteVoterScopeRegisteredOnly, ShowResult: VoteShowResultAfterEnd, MaxChoices: &mc, RequireCaptcha: false, }) require.Nil(t, err) got, err := db.GetByID(ctx, 1) require.Nil(t, err) require.Equal(t, "Title - updated", got.Title) require.Equal(t, VoteTypeMultiple, got.Type) require.Equal(t, VoteVisibilityLinkOnly, got.Visibility) require.Equal(t, VoteVoterScopeRegisteredOnly, got.VoterScope) require.Equal(t, VoteShowResultAfterEnd, got.ShowResult) require.Equal(t, 3, got.MaxChoices) require.False(t, got.RequireCaptcha) }) // 回归:调用方漏传 Type/MaxChoices(nil 指针)时,db.Update 必须跳过写入, // 不得把 type 写成 ""(曾导致发布后编辑使公开页报「未知的投票类型」)。 t.Run("omit type preserves existing", func(t *testing.T) { err := db.Update(ctx, 1, UpdateVoteOptions{Title: "Title - meta only"}) require.Nil(t, err) got, err := db.GetByID(ctx, 1) require.Nil(t, err) require.Equal(t, "Title - meta only", got.Title) require.Equal(t, VoteTypeMultiple, got.Type, "type 不应被清零") require.Equal(t, 3, got.MaxChoices, "max_choices 不应被清零") }) t.Run("not found", func(t *testing.T) { err := db.Update(ctx, 404, UpdateVoteOptions{Title: "x"}) require.Equal(t, ErrVoteNotExist, err) }) } func testVotesUpdateStatus(t *testing.T, ctx context.Context, db *votes) { _, err := db.Create(ctx, CreateVoteOptions{ UserID: 1, Title: "Title - 1", }) require.Nil(t, err) t.Run("normal", func(t *testing.T) { require.Nil(t, db.UpdateStatus(ctx, 1, VoteStatusPublished)) got, err := db.GetByID(ctx, 1) require.Nil(t, err) require.Equal(t, VoteStatusPublished, got.Status) require.Nil(t, db.UpdateStatus(ctx, 1, VoteStatusEnded)) got, err = db.GetByID(ctx, 1) require.Nil(t, err) require.Equal(t, VoteStatusEnded, got.Status) }) t.Run("not found", func(t *testing.T) { require.Equal(t, ErrVoteNotExist, db.UpdateStatus(ctx, 404, VoteStatusEnded)) }) } func testVotesDeleteByID(t *testing.T, ctx context.Context, db *votes) { t.Run("normal", func(t *testing.T) { _, err := db.Create(ctx, CreateVoteOptions{UserID: 1, Title: "Title - 1"}) require.Nil(t, err) require.Nil(t, db.DeleteByID(ctx, 1)) _, err = db.GetByID(ctx, 1) require.Equal(t, ErrVoteNotExist, err) }) t.Run("not found", func(t *testing.T) { require.Equal(t, ErrVoteNotExist, db.DeleteByID(ctx, 404)) }) } func testVotesCountByUserID(t *testing.T, ctx context.Context, db *votes) { _, err := db.Create(ctx, CreateVoteOptions{UserID: 1, Title: "A"}) require.Nil(t, err) _, err = db.Create(ctx, CreateVoteOptions{UserID: 1, Title: "B"}) require.Nil(t, err) _, err = db.Create(ctx, CreateVoteOptions{UserID: 2, Title: "C"}) require.Nil(t, err) got, err := db.CountByUserID(ctx, 1) require.Nil(t, err) require.Equal(t, int64(2), got) got, err = db.CountByUserID(ctx, 404) require.Nil(t, err) require.Equal(t, int64(0), got) }