309 lines
9.1 KiB
Go
309 lines
9.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"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/datatypes"
|
|
|
|
"github.com/tamakyi/TamaBox/internal/dbutil"
|
|
)
|
|
|
|
func TestVoteSubmissions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, cleanup := newTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
store := NewVoteSubmissionsStore(db)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
test func(t *testing.T, ctx context.Context, db *voteSubmissions)
|
|
}{
|
|
{"Create", testVoteSubmissionsCreate},
|
|
{"UniqueUser", testVoteSubmissionsUniqueUser},
|
|
{"UniqueToken", testVoteSubmissionsUniqueToken},
|
|
{"UserAndAnonCoexist", testVoteSubmissionsUserAndAnonCoexist},
|
|
{"CrossVote", testVoteSubmissionsCrossVote},
|
|
{"Count", testVoteSubmissionsCount},
|
|
{"HasUserVoted", testVoteSubmissionsHasUserVoted},
|
|
{"HasTokenVoted", testVoteSubmissionsHasTokenVoted},
|
|
{"CountByIP", testVoteSubmissionsCountByIP},
|
|
{"ListByVoteID", testVoteSubmissionsListByVoteID},
|
|
{"DeleteByVoteID", testVoteSubmissionsDeleteByVoteID},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Cleanup(func() {
|
|
if err := cleanup("vote_submissions"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
tc.test(t, ctx, store.(*voteSubmissions))
|
|
})
|
|
}
|
|
}
|
|
|
|
func userIDPtr(id uint) *uint { return &id }
|
|
func tokenPtr(s string) *string { return &s }
|
|
|
|
func testVoteSubmissionsCreate(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
got, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 2,
|
|
UserID: userIDPtr(10),
|
|
VoterIP: "114.5.1.4",
|
|
})
|
|
require.Nil(t, err)
|
|
require.Equal(t, uint(1), got.ID)
|
|
require.Equal(t, uint(10), *got.UserID)
|
|
}
|
|
|
|
// 核心防刷:同一登录用户对同一投票重复提交必须被数据库唯一索引拦截。
|
|
func testVoteSubmissionsUniqueUser(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 1,
|
|
UserID: userIDPtr(10),
|
|
VoterIP: "114.5.1.4",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("duplicate rejected", func(t *testing.T) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 2,
|
|
UserID: userIDPtr(10),
|
|
VoterIP: "114.5.1.4",
|
|
})
|
|
require.Equal(t, ErrVoteAlreadyVoted, err)
|
|
})
|
|
|
|
t.Run("different user allowed", func(t *testing.T) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 1,
|
|
UserID: userIDPtr(11),
|
|
VoterIP: "114.5.1.5",
|
|
})
|
|
require.Nil(t, err)
|
|
})
|
|
}
|
|
|
|
// 核心防刷:同一匿名指纹对同一投票重复提交必须被拦截。
|
|
func testVoteSubmissionsUniqueToken(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 1,
|
|
VoterToken: tokenPtr("fp-token-001"),
|
|
VoterIP: "114.5.1.4",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("duplicate rejected", func(t *testing.T) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 2,
|
|
VoterToken: tokenPtr("fp-token-001"),
|
|
VoterIP: "114.5.1.4",
|
|
})
|
|
require.Equal(t, ErrVoteAlreadyVoted, err)
|
|
})
|
|
|
|
t.Run("different token allowed", func(t *testing.T) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 1,
|
|
VoterToken: tokenPtr("fp-token-002"),
|
|
VoterIP: "114.5.1.4",
|
|
})
|
|
require.Nil(t, err)
|
|
})
|
|
}
|
|
|
|
// 登录用户与匿名指纹可在同一投票下并存(user_id 为 NULL 与 voter_token 为 NULL 不冲突)。
|
|
func testVoteSubmissionsUserAndAnonCoexist(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 1,
|
|
UserID: userIDPtr(10),
|
|
VoterIP: "114.5.1.4",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 2,
|
|
VoterToken: tokenPtr("fp-anon-001"),
|
|
VoterIP: "114.5.1.6",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
// 同一 user 换匿名 token 再来:user_id 相同仍应被拦截
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 1,
|
|
UserID: userIDPtr(10),
|
|
VoterToken: tokenPtr("fp-anon-002"),
|
|
VoterIP: "114.5.1.7",
|
|
})
|
|
require.Equal(t, ErrVoteAlreadyVoted, err)
|
|
|
|
count, err := db.CountByVoteID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(2), count)
|
|
}
|
|
|
|
// 同一用户 / 同一 token 对不同投票不受约束。
|
|
func testVoteSubmissionsCrossVote(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
for voteID := uint(1); voteID <= 2; voteID++ {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: voteID,
|
|
OptionID: 1,
|
|
UserID: userIDPtr(10),
|
|
VoterIP: "114.5.1.4",
|
|
})
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
// 同一 token 投两个不同投票也允许
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 1,
|
|
VoterToken: tokenPtr("fp-x"),
|
|
VoterIP: "114.5.1.8",
|
|
})
|
|
require.Nil(t, err)
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 2,
|
|
OptionID: 1,
|
|
VoterToken: tokenPtr("fp-x"),
|
|
VoterIP: "114.5.1.8",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
count, err := db.CountByVoteID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(2), count)
|
|
}
|
|
|
|
func testVoteSubmissionsCount(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
// 投票 1:选项 1 两票,选项 2 一票
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 1, UserID: userIDPtr(1), VoterIP: "a"})
|
|
require.Nil(t, err)
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 1, UserID: userIDPtr(2), VoterIP: "b"})
|
|
require.Nil(t, err)
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 2, UserID: userIDPtr(3), VoterIP: "c"})
|
|
require.Nil(t, err)
|
|
// 投票 2:一票
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 2, OptionID: 1, UserID: userIDPtr(4), VoterIP: "d"})
|
|
require.Nil(t, err)
|
|
|
|
t.Run("total", func(t *testing.T) {
|
|
got, err := db.CountByVoteID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(3), got)
|
|
})
|
|
|
|
t.Run("by option", func(t *testing.T) {
|
|
got, err := db.CountByOptionID(ctx, 1, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(2), got)
|
|
|
|
got, err = db.CountByOptionID(ctx, 1, 2)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(1), got)
|
|
})
|
|
|
|
t.Run("empty", func(t *testing.T) {
|
|
got, err := db.CountByVoteID(ctx, 404)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(0), got)
|
|
})
|
|
}
|
|
|
|
func testVoteSubmissionsHasUserVoted(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 1, UserID: userIDPtr(10), VoterIP: "a"})
|
|
require.Nil(t, err)
|
|
|
|
got, err := db.HasUserVoted(ctx, 1, 10)
|
|
require.Nil(t, err)
|
|
require.True(t, got)
|
|
|
|
got, err = db.HasUserVoted(ctx, 1, 11)
|
|
require.Nil(t, err)
|
|
require.False(t, got)
|
|
}
|
|
|
|
func testVoteSubmissionsHasTokenVoted(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 1, VoterToken: tokenPtr("fp-1"), VoterIP: "a"})
|
|
require.Nil(t, err)
|
|
|
|
got, err := db.HasTokenVoted(ctx, 1, "fp-1")
|
|
require.Nil(t, err)
|
|
require.True(t, got)
|
|
|
|
got, err = db.HasTokenVoted(ctx, 1, "fp-2")
|
|
require.Nil(t, err)
|
|
require.False(t, got)
|
|
}
|
|
|
|
func testVoteSubmissionsCountByIP(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
since := time.Now().Add(-1 * time.Hour)
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 1, UserID: userIDPtr(1), VoterIP: "1.2.3.4"})
|
|
require.Nil(t, err)
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 1, UserID: userIDPtr(2), VoterIP: "1.2.3.4"})
|
|
require.Nil(t, err)
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 1, UserID: userIDPtr(3), VoterIP: "5.6.7.8"})
|
|
require.Nil(t, err)
|
|
|
|
got, err := db.CountByIP(ctx, 1, "1.2.3.4", since)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(2), got)
|
|
|
|
got, err = db.CountByIP(ctx, 1, "5.6.7.8", since)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(1), got)
|
|
}
|
|
|
|
func testVoteSubmissionsListByVoteID(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
for i := 0; i < 3; i++ {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{
|
|
VoteID: 1,
|
|
OptionID: 1,
|
|
UserID: userIDPtr(uint(100 + i)),
|
|
VoterIP: "x",
|
|
FormData: datatypes.JSON(`{"nickname":"user` + string(rune('0'+i)) + `"}`),
|
|
})
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
got, err := db.ListByVoteID(ctx, 1, &dbutil.Cursor{Value: 2, PageSize: 10})
|
|
require.Nil(t, err)
|
|
require.Len(t, got, 1)
|
|
require.Equal(t, uint(1), got[0].ID, "游标取 id < 2 的记录")
|
|
}
|
|
|
|
func testVoteSubmissionsDeleteByVoteID(t *testing.T, ctx context.Context, db *voteSubmissions) {
|
|
_, err := db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 1, OptionID: 1, UserID: userIDPtr(1), VoterIP: "a"})
|
|
require.Nil(t, err)
|
|
_, err = db.Create(ctx, CreateVoteSubmissionOptions{VoteID: 2, OptionID: 1, UserID: userIDPtr(2), VoterIP: "b"})
|
|
require.Nil(t, err)
|
|
|
|
require.Nil(t, db.DeleteByVoteID(ctx, 1))
|
|
|
|
got, err := db.CountByVoteID(ctx, 1)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(0), got)
|
|
|
|
got, err = db.CountByVoteID(ctx, 2)
|
|
require.Nil(t, err)
|
|
require.Equal(t, int64(1), got)
|
|
}
|