Files
ttstd-infinite-canvas/backend/internal/repository/analytics_test.go

193 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package repository
import (
"context"
"strings"
"testing"
"time"
"infinite-canvas/backend/internal/model"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type sqlCaptureLogger struct {
statements []string
}
func TestAPICallLogRecordTypeFiltersListAndExport(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:api-log-record-types?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.ApiCallLog{}); err != nil {
t.Fatal(err)
}
now := time.Now()
for _, item := range []model.ApiCallLog{
{ID: "request", Capability: "video", RequestKind: "create", CreatedAt: now},
{ID: "poll", Capability: "video", RequestKind: "poll", CreatedAt: now},
{ID: "video-download", Capability: "video", RequestKind: "download", CreatedAt: now},
{ID: "image-download", Capability: "image", RequestKind: "download", CreatedAt: now},
{ID: "upload", Capability: "image", RequestKind: "upload", CreatedAt: now},
{ID: "local-save", Capability: "image", RequestKind: "local_save", CreatedAt: now},
{ID: "register", Capability: "image", RequestKind: "register", CreatedAt: now},
} {
if err := db.Create(&item).Error; err != nil {
t.Fatal(err)
}
}
repo := New(db)
for _, tc := range []struct {
kind string
count int
}{{"", 1}, {"request", 1}, {"download", 2}, {"all", 7}} {
filter := APICallLogFilter{AnalyticsFilter: AnalyticsFilter{From: now.Add(-time.Hour), To: now.Add(time.Hour)}, RecordType: tc.kind}
logs, total, err := repo.QueryAPICallLogs(filter)
if err != nil {
t.Fatal(err)
}
if len(logs) != tc.count || total != int64(tc.count) {
t.Fatalf("kind=%s: count=%d total=%d", tc.kind, len(logs), total)
}
exported, err := repo.ExportAPICallLogs(filter, 100)
if err != nil || len(exported) != tc.count {
t.Fatalf("kind=%s: export count=%d err=%v", tc.kind, len(exported), err)
}
}
}
func (l *sqlCaptureLogger) LogMode(logger.LogLevel) logger.Interface { return l }
func (*sqlCaptureLogger) Info(context.Context, string, ...any) {}
func (*sqlCaptureLogger) Warn(context.Context, string, ...any) {}
func (*sqlCaptureLogger) Error(context.Context, string, ...any) {}
func (l *sqlCaptureLogger) Trace(_ context.Context, _ time.Time, query func() (string, int64), _ error) {
statement, _ := query()
l.statements = append(l.statements, statement)
}
func TestRecordUserActivityQualifiesPostgresConflictColumns(t *testing.T) {
capture := &sqlCaptureLogger{}
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: "host=localhost user=test dbname=test sslmode=disable",
PreferSimpleProtocol: true,
}), &gorm.Config{
DisableAutomaticPing: true,
DryRun: true,
Logger: capture,
SkipDefaultTransaction: true,
})
if err != nil {
t.Fatalf("open dry-run postgres database: %v", err)
}
repo := New(db)
cases := []struct {
event string
column string
}{
{event: "login", column: "login_count"},
{event: "task", column: "task_count"},
{event: "agent_message", column: "agent_message_count"},
{event: "asset", column: "asset_count"},
{event: "resource", column: "resource_count"},
}
for _, tc := range cases {
t.Run(tc.event, func(t *testing.T) {
capture.statements = nil
if err := repo.RecordUserActivity("user-1", tc.event, 1, time.Unix(1_700_000_000, 0)); err != nil {
t.Fatalf("record activity: %v", err)
}
if len(capture.statements) == 0 {
t.Fatal("expected generated SQL")
}
statement := capture.statements[len(capture.statements)-1]
if !strings.Contains(statement, "user_daily_activities."+tc.column) {
t.Fatalf("conflict update column is not target-qualified: %s", statement)
}
if tc.event != "login" && !strings.Contains(statement, "COALESCE(user_daily_activities.first_active_at") {
t.Fatalf("first active column is not target-qualified: %s", statement)
}
})
}
}
func TestQueryAPICallLogsSearchesFailureFields(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:api-log-error-search?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.User{}, &model.ModelChannel{}, &model.ApiCallLog{}); err != nil {
t.Fatal(err)
}
now := time.Now()
item := model.ApiCallLog{
ID: "api-log-1",
UserID: "user-1",
RequestKind: "create",
Status: model.ApiCallStatusFailed,
ErrorCode: "request_not_sent",
Error: "模型服务拒绝了请求,请检查模型和参数;上游:invalid parameter: size",
CreatedAt: now,
}
if err := db.Create(&item).Error; err != nil {
t.Fatal(err)
}
repo := New(db)
for _, keyword := range []string{"模型服务拒绝", "invalid parameter", "request_not_sent"} {
logs, total, err := repo.QueryAPICallLogs(APICallLogFilter{
AnalyticsFilter: AnalyticsFilter{From: now.Add(-time.Hour), To: now.Add(time.Hour)},
Keyword: keyword,
Page: 1,
Limit: 20,
})
if err != nil {
t.Fatalf("QueryAPICallLogs(%q) error = %v", keyword, err)
}
if total != 1 || len(logs) != 1 || logs[0].ID != item.ID {
t.Fatalf("QueryAPICallLogs(%q) = total:%d logs:%#v", keyword, total, logs)
}
}
}
func TestQueryAPICallLogsHidesInternalPollStages(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:api-log-visible-stages?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.ApiCallLog{}); err != nil {
t.Fatal(err)
}
now := time.Now()
logs := []model.ApiCallLog{
{ID: "image-create", UserID: "user-1", Capability: "image", RequestKind: "create", CreatedAt: now},
{ID: "image-poll", UserID: "user-1", Capability: "image", RequestKind: "poll", CreatedAt: now.Add(time.Second)},
{ID: "image-download", UserID: "user-1", Capability: "image", RequestKind: "download", CreatedAt: now.Add(2 * time.Second)},
{ID: "video-create", UserID: "user-1", Capability: "video", RequestKind: "create", CreatedAt: now.Add(3 * time.Second)},
{ID: "video-poll", UserID: "user-1", Capability: "video", RequestKind: "poll", CreatedAt: now.Add(4 * time.Second)},
}
if err := db.Create(&logs).Error; err != nil {
t.Fatal(err)
}
items, total, err := New(db).QueryAPICallLogs(APICallLogFilter{
AnalyticsFilter: AnalyticsFilter{From: now.Add(-time.Hour), To: now.Add(time.Hour)},
Page: 1,
Limit: 20,
})
if err != nil {
t.Fatal(err)
}
if total != 2 || len(items) != 2 {
t.Fatalf("visible logs = total:%d items:%#v, want only create logs without polls or downloads", total, items)
}
if items[0].ID != "video-create" || items[1].ID != "image-create" {
t.Fatalf("visible logs = %#v, want video-create and image-create", items)
}
}