* 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>
452 lines
12 KiB
Go
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)
|
|
}
|