// 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" ) // TestReports 覆盖举报写入的幂等性与两种计数口径。 // // 幂等必须由唯一索引 + isDuplicateKeyError 兜住:如果改成「先查后插」, // 并发下仍会撞索引报 500;而如果直接把冲突当错误返回,举报按钮会变成 // 「这条内容是否存在」的探测接口(第二次举报报错 = 内容存在)。 func TestReports(t *testing.T) { database, cleanup := newTestDB(t) ctx := context.Background() t.Cleanup(func() { if err := cleanup("users", "questions", "content_reports"); err != nil { t.Fatal(err) } }) usersStore := NewUsersStore(database) questionsStore := NewQuestionsStore(database) reportsStore := NewReportsStore(database) require.NoError(t, usersStore.Create(ctx, CreateUserOptions{ Name: "Box", Password: "super_secret", Email: "rp@example.com", Domain: "rpbox", })) owner, err := usersStore.GetByDomain(ctx, "rpbox") require.NoError(t, err) question, err := questionsStore.Create(ctx, CreateQuestionOptions{ FromIP: "1.1.1.1", UserID: owner.ID, Content: "被举报的内容", }) require.NoError(t, err) require.NoError(t, questionsStore.AnswerByID(ctx, question.ID, "回答")) // 首次举报写入成功。 created, err := reportsStore.Create(ctx, CreateReportOptions{ QuestionID: question.ID, ReporterIP: "2.2.2.2", Reason: ReportReasonHarassment, }) require.NoError(t, err) require.True(t, created) // 同 IP 再报一次:幂等成功(不报错、不重复计数)。 created, err = reportsStore.Create(ctx, CreateReportOptions{ QuestionID: question.ID, ReporterIP: "2.2.2.2", Reason: ReportReasonDoxxing, }) require.NoError(t, err) require.False(t, created, "duplicate (question_id, reporter_ip) must be idempotent") // 另一个 IP 正常计入。 created, err = reportsStore.Create(ctx, CreateReportOptions{ QuestionID: question.ID, ReporterIP: "3.3.3.3", Reason: ReportReasonDivisive, }) require.NoError(t, err) require.True(t, created) counts, err := reportsStore.QuestionIDsByUser(ctx, owner.ID) require.NoError(t, err) require.Equal(t, map[uint]int64{question.ID: 2}, counts) count24, err := reportsStore.CountByUserSince(ctx, owner.ID, time.Now().Add(-24*time.Hour)) require.NoError(t, err) require.EqualValues(t, 2, count24) // 窗口之外(未来时间点起算)应为 0,保证告警不会用错时间范围。 countFuture, err := reportsStore.CountByUserSince(ctx, owner.ID, time.Now().Add(time.Hour)) require.NoError(t, err) require.EqualValues(t, 0, countFuture) // 已驳回的不计入箱主侧标记与告警。 require.NoError(t, database.Model(&ContentReport{}). Where("question_id = ?", question.ID). Update("status", ReportStatusRejected).Error) counts, err = reportsStore.QuestionIDsByUser(ctx, owner.ID) require.NoError(t, err) require.Empty(t, counts) }