// Copyright 2022 E99p1ant. 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" "strings" "testing" "time" "github.com/stretchr/testify/require" ) // TestSiteAnalytics 验证 /stats 页的聚合口径。 // // 这里刻意连真库跑而不是塞 mock:整段都是数据库方言相关的 SQL // (to_char / EXTRACT / CHAR_LENGTH / SUM(CASE WHEN)), // 对着 mock 断言等于什么都没验证。 func TestSiteAnalytics(t *testing.T) { t.Parallel() db, cleanup := newTestDB(t) t.Cleanup(func() { if err := cleanup("users", "questions", "messages"); err != nil { t.Fatal(err) } }) ctx := context.Background() now := time.Now() store := NewSiteAnalyticsStore(db) questions := NewQuestionsStore(db) require.NoError(t, db.Create(&User{UID: "u1", Name: "box", Domain: "boxa", Email: "boxa@example.com"}).Error) // 一条登录用户提的、已回答的长问题 answered, err := questions.Create(ctx, CreateQuestionOptions{ UserID: 1, Content: strings.Repeat("字", 120), FromIP: "1.1.1.1", AskerUserID: 7, }) require.NoError(t, err) require.NoError(t, questions.AnswerByID(ctx, answered.ID, "答案")) // 一条匿名提的私密短问题,未读未答 _, err = questions.Create(ctx, CreateQuestionOptions{ UserID: 1, Content: "短问题", FromIP: "1.1.1.2", IsPrivate: true, }) require.NoError(t, err) // 一条被软删除的:不该出现在任何数字里 deleted, err := questions.Create(ctx, CreateQuestionOptions{ UserID: 1, Content: "会被删掉", FromIP: "1.1.1.3", }) require.NoError(t, err) require.NoError(t, questions.DeleteByID(ctx, deleted.ID)) got, err := store.SiteAnalytics(ctx, now) require.NoError(t, err) // 计数 require.EqualValues(t, 1, got.RegisteredUsers) require.EqualValues(t, 2, got.TotalQuestions, "软删除的提问不能计入总数") require.EqualValues(t, 1, got.Answered) require.EqualValues(t, 1, got.Private) require.EqualValues(t, 1, got.Read, "已回答的提问恒为已读") require.EqualValues(t, 1, got.Anonymous) require.EqualValues(t, 1, got.ActiveBoxes) require.EqualValues(t, 1, got.AnsweringBoxes) // 今日(提问是刚才写的,必然落在今天) require.EqualValues(t, 2, got.TodayQuestions) require.EqualValues(t, 1, got.TodayAnswers) // 日/月趋势里能找到今天与本月 today := now.Format("2006-01-02") require.EqualValues(t, 2, countOf(got.Daily, today)) require.EqualValues(t, 2, countOf(got.Monthly, now.Format("2006-01"))) // 热力图:两条提问都发生在当前这一小时 dow := int(now.Weekday()) // Go 的 Weekday 与 EXTRACT(DOW) 都是周日 0 require.EqualValues(t, 2, got.Heatmap[dow][now.Hour()]) // 字数分桶:120 字落在 100–199(下标 3),"短问题" 3 字落在 <20(下标 0) require.EqualValues(t, 1, got.LengthBins[0]) require.EqualValues(t, 1, got.LengthBins[3]) // 回答耗时:刚回答完,落在 1 小时内(下标 0);未回答的不计入 require.EqualValues(t, 1, got.ReplyBins[0]) var replyTotal int64 for _, n := range got.ReplyBins { replyTotal += n } require.EqualValues(t, 1, replyTotal, "耗时分布只统计已回答的提问") // 最后时间戳 require.NotNil(t, got.LastQuestionAt) require.NotNil(t, got.LastAnswerAt) } // TestSiteAnalyticsEmpty 空库不应报错,也不应把零值算成异常。 func TestSiteAnalyticsEmpty(t *testing.T) { t.Parallel() db, cleanup := newTestDB(t) t.Cleanup(func() { if err := cleanup("users", "questions", "messages"); err != nil { t.Fatal(err) } }) got, err := NewSiteAnalyticsStore(db).SiteAnalytics(context.Background(), time.Now()) require.NoError(t, err) require.Zero(t, got.TotalQuestions) require.Zero(t, got.RegisteredUsers) require.Nil(t, got.LastQuestionAt) require.Nil(t, got.LastAnswerAt) var heatTotal int64 for _, row := range got.Heatmap { for _, n := range row { heatTotal += n } } require.Zero(t, heatTotal) } func countOf(points []CountPoint, key string) int64 { for _, p := range points { if p.Key == key { return p.N } } return 0 }