Repo .git was damaged (refs/ missing, index truncated, object store near-empty). Repaired via git init (rebuilt refs), stale index preserved as .git/index.corrupt-20260910.bak, then full worktree committed as a rollback baseline.
253 lines
7.1 KiB
Go
253 lines
7.1 KiB
Go
// 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)
|
||
}
|