Files
gochat/backend/internal/reporting/reporting_test.go
T
Rogeeandrogee 2b182f9956 H-300: wire Captain Skills into Web runtime (#48)
* H-300: wire Captain Skills into Web runtime

* H-300: enforce effective model and conservative skill budget

* H-300: fix CI gosec step

* ci: extend golangci-lint timeout

* fix lint findings across backend

* fix(push): resolve delivery protocol blockers

* test(repository): close SQLite test databases

* test(repository): reuse SQLite schema per package

* H-307: restore backend Go cache in CI

* H-307: prefetch modules before cold lint

* H-307: resolve govulncheck security gate

* H-307: build lint with patched Go toolchain

* H-307: clear remaining security scan findings

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-19 07:08:14 +08:00

452 lines
12 KiB
Go

package reporting
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/channel"
"github.com/gochat/gochat/internal/model"
)
func newTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
err = db.AutoMigrate(
&model.ReportingEvent{},
&model.ReportingEventsRollup{},
&model.Conversation{},
&model.Message{},
)
require.NoError(t, err)
return db
}
func TestNewReportingService(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
require.NotNil(t, svc)
assert.Equal(t, db, svc.db)
}
func TestReportingService_CreateEvent(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
event := &ReportingEvent{
AccountID: 1,
Name: "first_response",
Value: 120.5,
EventStartTime: now,
EventEndTime: now,
}
err := svc.CreateEvent(ctx, event)
require.NoError(t, err)
assert.NotZero(t, event.ID)
}
func TestReportingService_GetByID(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
event := &ReportingEvent{
AccountID: 1,
Name: "first_response",
Value: 100.0,
EventStartTime: now,
EventEndTime: now,
}
err := svc.CreateEvent(ctx, event)
require.NoError(t, err)
retrieved, err := svc.GetEvent(ctx, event.ID)
require.NoError(t, err)
assert.Equal(t, event.Name, retrieved.Name)
assert.Equal(t, event.Value, retrieved.Value)
}
func TestReportingService_GetByID_NotFound(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
_, err := svc.GetEvent(ctx, 99999)
assert.Error(t, err)
assert.ErrorIs(t, err, gorm.ErrRecordNotFound)
}
func TestReportingService_ListEventsByAccount(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
for i := 0; i < 3; i++ {
err := svc.CreateEvent(ctx, &ReportingEvent{
AccountID: 1,
Name: "first_response",
Value: float64(i * 10),
EventStartTime: now,
EventEndTime: now,
})
require.NoError(t, err)
}
// Different account
err := svc.CreateEvent(ctx, &ReportingEvent{
AccountID: 2,
Name: "first_response",
Value: 0,
EventStartTime: now,
EventEndTime: now,
})
require.NoError(t, err)
events, err := svc.ListEventsByAccount(ctx, 1, now.Add(-time.Hour), now.Add(time.Hour))
require.NoError(t, err)
assert.Len(t, events, 3)
}
func TestReportingService_ListEventsByAccountAndName(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
require.NoError(t, svc.CreateEvent(ctx, &ReportingEvent{
AccountID: 1, Name: "first_response", Value: 100, EventStartTime: now, EventEndTime: now,
}))
require.NoError(t, svc.CreateEvent(ctx, &ReportingEvent{
AccountID: 1, Name: "resolution_time", Value: 200, EventStartTime: now, EventEndTime: now,
}))
require.NoError(t, svc.CreateEvent(ctx, &ReportingEvent{
AccountID: 1, Name: "first_response", Value: 150, EventStartTime: now, EventEndTime: now,
}))
events, err := svc.ListEventsByAccountAndName(ctx, 1, "first_response", now.Add(-time.Hour), now.Add(time.Hour))
require.NoError(t, err)
assert.Len(t, events, 2)
}
func TestReportingService_CreateRollup(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
rollup := &ReportingEventsRollup{
AccountID: 1,
Date: now,
DimensionType: DimensionAccount,
DimensionID: 1,
Metric: MetricResolutionsCount,
Count: 5,
SumValue: 100.0,
SumValueBusinessHours: 80.0,
}
err := svc.CreateRollup(ctx, rollup)
require.NoError(t, err)
assert.NotZero(t, rollup.ID)
}
func TestReportingService_CreateRollup_Upsert(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
rollup := &ReportingEventsRollup{
AccountID: 1,
Date: now,
DimensionType: DimensionAccount,
DimensionID: 1,
Metric: MetricResolutionsCount,
Count: 5,
SumValue: 100.0,
SumValueBusinessHours: 80.0,
}
err := svc.CreateRollup(ctx, rollup)
require.NoError(t, err)
// Upsert with same unique key — should update not create new
rollup2 := &ReportingEventsRollup{
AccountID: 1,
Date: now,
DimensionType: DimensionAccount,
DimensionID: 1,
Metric: MetricResolutionsCount,
Count: 10,
SumValue: 200.0,
SumValueBusinessHours: 160.0,
}
err = svc.CreateRollup(ctx, rollup2)
require.NoError(t, err)
// Should still be only one record
rollups, err := svc.GetRollups(ctx, 1, DimensionAccount, 1, now.Add(-time.Hour), now.Add(time.Hour))
require.NoError(t, err)
assert.Len(t, rollups, 1)
}
func TestReportingService_GetRollups(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
require.NoError(t, svc.CreateRollup(ctx, &ReportingEventsRollup{
AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 5,
}))
require.NoError(t, svc.CreateRollup(ctx, &ReportingEventsRollup{
AccountID: 1, Date: now, DimensionType: DimensionAgent, DimensionID: 2, Metric: MetricFirstResponse, Count: 3,
}))
rollups, err := svc.GetRollups(ctx, 1, DimensionAccount, 1, now.Add(-time.Hour), now.Add(time.Hour))
require.NoError(t, err)
assert.Len(t, rollups, 1)
assert.Equal(t, MetricResolutionsCount, rollups[0].Metric)
}
func TestReportingService_GetRollupsByMetric(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
require.NoError(t, svc.CreateRollup(ctx, &ReportingEventsRollup{
AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 5,
}))
require.NoError(t, svc.CreateRollup(ctx, &ReportingEventsRollup{
AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricFirstResponse, Count: 3,
}))
rollups, err := svc.GetRollupsByMetric(ctx, 1, DimensionAccount, 1, MetricResolutionsCount, now.Add(-time.Hour), now.Add(time.Hour))
require.NoError(t, err)
assert.Len(t, rollups, 1)
assert.Equal(t, MetricResolutionsCount, rollups[0].Metric)
}
func TestReportingService_DeleteRollupsByDate(t *testing.T) {
db := newTestDB(t)
svc := NewReportingService(db)
ctx := context.Background()
now := time.Now()
err := svc.CreateRollup(ctx, &ReportingEventsRollup{
AccountID: 1, Date: now, DimensionType: DimensionAccount, DimensionID: 1, Metric: MetricResolutionsCount, Count: 5,
})
require.NoError(t, err)
err = svc.DeleteRollupsByDate(ctx, 1, now.Add(-time.Hour), now.Add(time.Hour))
require.NoError(t, err)
rollups, err := svc.GetRollups(ctx, 1, DimensionAccount, 1, now.Add(-time.Hour), now.Add(time.Hour))
require.NoError(t, err)
assert.Empty(t, rollups)
}
func TestNewRawDataSource(t *testing.T) {
ds := NewRawDataSource()
require.NotNil(t, ds)
}
func TestGetMetricDefinition_Found(t *testing.T) {
def, ok := GetMetricDefinition("avg_first_response_time")
require.True(t, ok)
assert.Equal(t, AggregateAverage, def.AggregateType)
assert.Equal(t, "first_response", def.RawEventName)
assert.Equal(t, MetricFirstResponse, def.RollupMetric)
}
func TestGetMetricDefinition_NotFound(t *testing.T) {
_, ok := GetMetricDefinition("nonexistent_metric")
assert.False(t, ok)
}
func TestAllMetricKeys(t *testing.T) {
keys := AllMetricKeys()
assert.NotEmpty(t, keys)
// Should contain all registered metrics
assert.Contains(t, keys, "avg_first_response_time")
assert.Contains(t, keys, "avg_resolution_time")
assert.Contains(t, keys, "resolutions_count")
}
func TestReportMetricRegistry_AllMetricsHaveDefinitions(t *testing.T) {
for key, def := range ReportMetricRegistry {
assert.NotEmpty(t, def.RawEventName, "metric %s should have RawEventName", key)
assert.NotEmpty(t, def.SummaryKey, "metric %s should have SummaryKey", key)
assert.NotEmpty(t, string(def.RollupMetric), "metric %s should have RollupMetric", key)
}
}
// --- ReportingEventListener Tests ---
func TestNewReportingEventListener(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
require.NotNil(t, l)
assert.Equal(t, "reporting_event_listener", l.Name())
}
func TestReportingEventListener_OnEvent_UnknownEvent(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
ctx := context.Background()
event := &channel.ChannelEvent{Type: "unknown.event", Data: map[string]interface{}{}}
err := l.OnEvent(ctx, event)
assert.NoError(t, err)
}
func TestReportingEventListener_OnConversationCreated(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
ctx := context.Background()
conv := &model.Conversation{
Base: model.Base{ID: 1},
AccountID: 10,
InboxID: 20,
Status: "open",
}
event := &channel.ChannelEvent{
Type: channel.EventConversationCreated,
AccountID: 10,
InboxID: 20,
Data: map[string]interface{}{"conversation": conv},
}
err := l.OnEvent(ctx, event)
require.NoError(t, err)
// Verify reporting event was created
var count int64
l.db.WithContext(ctx).Model(&model.ReportingEvent{}).Where("account_id = ?", 10).Count(&count)
assert.True(t, count > 0, "should have created a reporting event")
}
func TestReportingEventListener_OnConversationCreated_NoConversation(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
ctx := context.Background()
event := &channel.ChannelEvent{
Type: channel.EventConversationCreated,
Data: map[string]interface{}{},
}
err := l.OnEvent(ctx, event)
assert.Error(t, err)
}
func TestReportingEventListener_OnConversationResolved(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
ctx := context.Background()
conv := &model.Conversation{
Base: model.Base{ID: 1},
AccountID: 10,
InboxID: 20,
Status: "resolved",
}
event := &channel.ChannelEvent{
Type: channel.EventConversationResolved,
AccountID: 10,
InboxID: 20,
Data: map[string]interface{}{"conversation": conv},
}
err := l.OnEvent(ctx, event)
require.NoError(t, err)
}
func TestReportingEventListener_OnConversationAssigned(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
ctx := context.Background()
conv := &model.Conversation{
Base: model.Base{ID: 1},
AccountID: 10,
InboxID: 20,
Status: "open",
}
event := &channel.ChannelEvent{
Type: channel.EventConversationAssigned,
AccountID: 10,
InboxID: 20,
Data: map[string]interface{}{"conversation": conv},
}
err := l.OnEvent(ctx, event)
require.NoError(t, err)
}
func TestReportingEventListener_OnMessageCreated_Outgoing(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
ctx := context.Background()
msg := &model.Message{
Base: model.Base{ID: 1},
ConversationID: 1,
AccountID: 10,
InboxID: 20,
MessageType: "outgoing",
SenderType: "agent",
}
event := &channel.ChannelEvent{
Type: channel.EventMessageCreated,
AccountID: 10,
InboxID: 20,
Data: map[string]interface{}{"message": msg},
}
err := l.OnEvent(ctx, event)
require.NoError(t, err)
}
func TestReportingEventListener_OnMessageCreated_Incoming(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
ctx := context.Background()
msg := &model.Message{
Base: model.Base{ID: 1},
ConversationID: 1,
AccountID: 10,
InboxID: 20,
MessageType: "incoming",
SenderType: "contact",
}
event := &channel.ChannelEvent{
Type: channel.EventMessageCreated,
AccountID: 10,
InboxID: 20,
Data: map[string]interface{}{"message": msg},
}
err := l.OnEvent(ctx, event)
// Incoming messages should be ignored — no error, no event
assert.NoError(t, err)
}
func TestReportingEventListener_OnMessageCreated_NoMessage(t *testing.T) {
db := newTestDB(t)
l := NewReportingEventListener(db)
ctx := context.Background()
event := &channel.ChannelEvent{
Type: channel.EventMessageCreated,
Data: map[string]interface{}{},
}
err := l.OnEvent(ctx, event)
// No message in event — returns nil (not all message events carry a message)
assert.NoError(t, err)
}