370 lines
10 KiB
Go
370 lines
10 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bridge/ai-customer-service/internal/domain/ticket"
|
|
)
|
|
|
|
func TestTicketWorkflowStore_ListOpen(t *testing.T) {
|
|
db := openDBForTest(t)
|
|
defer db.Close()
|
|
|
|
sessionStore := NewSessionStore(db)
|
|
ticketStore := NewTicketStore(db)
|
|
auditStore := NewAuditStore(db)
|
|
workflowStore := NewTicketWorkflowStore(db, auditStore)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
|
|
// Create session
|
|
sess, err := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
|
|
if err != nil {
|
|
t.Fatalf("failed to create session: %v", err)
|
|
}
|
|
|
|
// Create tickets with different statuses
|
|
openTicket := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user1",
|
|
Priority: ticket.PriorityP1,
|
|
Status: ticket.StatusOpen,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
assignedTicket := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user2",
|
|
Priority: ticket.PriorityP2,
|
|
Status: ticket.StatusAssigned,
|
|
AssignedTo: "agent1",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
resolvedTicket := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user3",
|
|
Priority: ticket.PriorityP3,
|
|
Status: ticket.StatusResolved,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
ticketStore.Create(ctx, openTicket)
|
|
ticketStore.Create(ctx, assignedTicket)
|
|
ticketStore.Create(ctx, resolvedTicket)
|
|
|
|
// ListOpen should return open + assigned (not resolved)
|
|
openList, err := workflowStore.ListOpen(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("ListOpen() error = %v", err)
|
|
}
|
|
if len(openList) < 2 {
|
|
t.Errorf("ListOpen() len = %d, want >= 2 (open + assigned)", len(openList))
|
|
}
|
|
}
|
|
|
|
func TestTicketWorkflowStore_Assign(t *testing.T) {
|
|
db := openDBForTest(t)
|
|
defer db.Close()
|
|
|
|
sessionStore := NewSessionStore(db)
|
|
ticketStore := NewTicketStore(db)
|
|
auditStore := NewAuditStore(db)
|
|
workflowStore := NewTicketWorkflowStore(db, auditStore)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
|
|
// Create session + open ticket
|
|
sess, err := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
|
|
if err != nil {
|
|
t.Fatalf("failed to create session: %v", err)
|
|
}
|
|
|
|
tkt := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user1",
|
|
Priority: ticket.PriorityP1,
|
|
Status: ticket.StatusOpen,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if err := ticketStore.Create(ctx, tkt); err != nil {
|
|
t.Fatalf("Create() error = %v", err)
|
|
}
|
|
|
|
// Assign it
|
|
err = workflowStore.Assign(ctx, tkt.ID, "agent-001", "admin", "127.0.0.1", now)
|
|
if err != nil {
|
|
t.Fatalf("Assign() error = %v", err)
|
|
}
|
|
|
|
// Verify assigned
|
|
fetched, _ := ticketStore.GetByID(ctx, tkt.ID)
|
|
if fetched.Status != ticket.StatusAssigned {
|
|
t.Errorf("ticket.Status = %s, want %s", fetched.Status, ticket.StatusAssigned)
|
|
}
|
|
if fetched.AssignedTo != "agent-001" {
|
|
t.Errorf("ticket.AssignedTo = %s, want agent-001", fetched.AssignedTo)
|
|
}
|
|
}
|
|
|
|
func TestTicketWorkflowStore_Assign_AlreadyAssigned(t *testing.T) {
|
|
db := openDBForTest(t)
|
|
defer db.Close()
|
|
|
|
sessionStore := NewSessionStore(db)
|
|
ticketStore := NewTicketStore(db)
|
|
auditStore := NewAuditStore(db)
|
|
workflowStore := NewTicketWorkflowStore(db, auditStore)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
|
|
// Create session + assigned ticket
|
|
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
|
|
tkt := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user1",
|
|
Priority: ticket.PriorityP1,
|
|
Status: ticket.StatusAssigned,
|
|
AssignedTo: "agent-001",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
ticketStore.Create(ctx, tkt)
|
|
|
|
// Try to assign again — should fail
|
|
err := workflowStore.Assign(ctx, tkt.ID, "agent-002", "admin", "127.0.0.1", now)
|
|
if err == nil {
|
|
t.Fatal("Assign() on already-assigned ticket should return error")
|
|
}
|
|
}
|
|
|
|
func TestTicketWorkflowStore_Resolve(t *testing.T) {
|
|
db := openDBForTest(t)
|
|
defer db.Close()
|
|
|
|
sessionStore := NewSessionStore(db)
|
|
ticketStore := NewTicketStore(db)
|
|
auditStore := NewAuditStore(db)
|
|
workflowStore := NewTicketWorkflowStore(db, auditStore)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
|
|
// Create session + assigned ticket
|
|
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
|
|
tkt := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user1",
|
|
Priority: ticket.PriorityP1,
|
|
Status: ticket.StatusAssigned,
|
|
AssignedTo: "agent-001",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
ticketStore.Create(ctx, tkt)
|
|
|
|
// Resolve it
|
|
err := workflowStore.Resolve(ctx, tkt.ID, "user satisfied", "admin", "127.0.0.1", now)
|
|
if err != nil {
|
|
t.Fatalf("Resolve() error = %v", err)
|
|
}
|
|
|
|
// Verify resolved
|
|
fetched, _ := ticketStore.GetByID(ctx, tkt.ID)
|
|
if fetched.Status != ticket.StatusResolved {
|
|
t.Errorf("ticket.Status = %s, want %s", fetched.Status, ticket.StatusResolved)
|
|
}
|
|
if fetched.Resolution != "user satisfied" {
|
|
t.Errorf("ticket.Resolution = %s, want 'user satisfied'", fetched.Resolution)
|
|
}
|
|
if fetched.ResolvedAt == nil {
|
|
t.Error("ticket.ResolvedAt should be set")
|
|
}
|
|
}
|
|
|
|
func TestTicketWorkflowStore_Resolve_ClosedTicketConflict(t *testing.T) {
|
|
db := openDBForTest(t)
|
|
defer db.Close()
|
|
|
|
sessionStore := NewSessionStore(db)
|
|
ticketStore := NewTicketStore(db)
|
|
auditStore := NewAuditStore(db)
|
|
workflowStore := NewTicketWorkflowStore(db, auditStore)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
resolvedTime := now.Add(-1 * time.Hour)
|
|
|
|
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
|
|
tkt := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user1",
|
|
Priority: ticket.PriorityP1,
|
|
Status: ticket.StatusClosed,
|
|
Resolution: "done",
|
|
ResolvedAt: &resolvedTime,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
ticketStore.Create(ctx, tkt)
|
|
|
|
err := workflowStore.Resolve(ctx, tkt.ID, "retry", "admin", "127.0.0.1", now)
|
|
if err == nil {
|
|
t.Fatal("Resolve() on closed ticket should return error")
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "CS_TICKET_4092") {
|
|
t.Fatalf("Resolve() error = %v, want CS_TICKET_4092 prefix", err)
|
|
}
|
|
}
|
|
|
|
func TestTicketWorkflowStore_Close(t *testing.T) {
|
|
db := openDBForTest(t)
|
|
defer db.Close()
|
|
|
|
sessionStore := NewSessionStore(db)
|
|
ticketStore := NewTicketStore(db)
|
|
auditStore := NewAuditStore(db)
|
|
workflowStore := NewTicketWorkflowStore(db, auditStore)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
|
|
// Create session + resolved ticket
|
|
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
|
|
resolvedTime := now.Add(-1 * time.Hour)
|
|
tkt := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user1",
|
|
Priority: ticket.PriorityP1,
|
|
Status: ticket.StatusResolved,
|
|
Resolution: "fixed",
|
|
ResolvedAt: &resolvedTime,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
ticketStore.Create(ctx, tkt)
|
|
|
|
// Close it
|
|
err := workflowStore.Close(ctx, tkt.ID, "user confirmed", "admin", "127.0.0.1", now)
|
|
if err != nil {
|
|
t.Fatalf("Close() error = %v", err)
|
|
}
|
|
|
|
// Verify closed
|
|
fetched, _ := ticketStore.GetByID(ctx, tkt.ID)
|
|
if fetched.Status != ticket.StatusClosed {
|
|
t.Errorf("ticket.Status = %s, want %s", fetched.Status, ticket.StatusClosed)
|
|
}
|
|
}
|
|
|
|
func TestTicketWorkflowStore_Close_NotResolved(t *testing.T) {
|
|
db := openDBForTest(t)
|
|
defer db.Close()
|
|
|
|
sessionStore := NewSessionStore(db)
|
|
ticketStore := NewTicketStore(db)
|
|
auditStore := NewAuditStore(db)
|
|
workflowStore := NewTicketWorkflowStore(db, auditStore)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
|
|
// Create session + open ticket (not resolved)
|
|
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
|
|
tkt := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user1",
|
|
Priority: ticket.PriorityP1,
|
|
Status: ticket.StatusOpen,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
ticketStore.Create(ctx, tkt)
|
|
|
|
// Try to close — should fail
|
|
err := workflowStore.Close(ctx, tkt.ID, "user confirmed", "admin", "127.0.0.1", now)
|
|
if err == nil {
|
|
t.Fatal("Close() on non-resolved ticket should return error")
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "CS_TICKET_4093") {
|
|
t.Fatalf("Close() error = %v, want CS_TICKET_4093 prefix", err)
|
|
}
|
|
}
|
|
|
|
func TestTicketWorkflowStore_Close_AssignedTicketConflict(t *testing.T) {
|
|
db := openDBForTest(t)
|
|
defer db.Close()
|
|
|
|
sessionStore := NewSessionStore(db)
|
|
ticketStore := NewTicketStore(db)
|
|
auditStore := NewAuditStore(db)
|
|
workflowStore := NewTicketWorkflowStore(db, auditStore)
|
|
ctx := context.Background()
|
|
now := time.Now().Truncate(time.Second)
|
|
|
|
sess, _ := sessionStore.GetOrCreate(ctx, "widget", uniqueID("user"), now)
|
|
tkt := &ticket.Ticket{
|
|
ID: uniqueID("tick"),
|
|
SessionID: sess.ID,
|
|
UserID: "user1",
|
|
Priority: ticket.PriorityP1,
|
|
Status: ticket.StatusAssigned,
|
|
AssignedTo: "agent-001",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
ticketStore.Create(ctx, tkt)
|
|
|
|
err := workflowStore.Close(ctx, tkt.ID, "premature close", "admin", "127.0.0.1", now)
|
|
if err == nil {
|
|
t.Fatal("Close() on assigned ticket should return error")
|
|
}
|
|
if !strings.HasPrefix(err.Error(), "CS_TICKET_4093") {
|
|
t.Fatalf("Close() error = %v, want CS_TICKET_4093 prefix", err)
|
|
}
|
|
}
|
|
|
|
func TestTicketWorkflowStore_NilDB(t *testing.T) {
|
|
workflowStore := NewTicketWorkflowStore(nil, nil)
|
|
ctx := context.Background()
|
|
now := time.Now()
|
|
|
|
t.Run("ListOpen returns error", func(t *testing.T) {
|
|
_, err := workflowStore.ListOpen(ctx, 10)
|
|
if err == nil {
|
|
t.Error("ListOpen() with nil db should return error")
|
|
}
|
|
})
|
|
|
|
t.Run("Assign returns error", func(t *testing.T) {
|
|
err := workflowStore.Assign(ctx, "t1", "agent1", "admin", "127.0.0.1", now)
|
|
if err == nil {
|
|
t.Error("Assign() with nil db should return error")
|
|
}
|
|
})
|
|
|
|
t.Run("Resolve returns error", func(t *testing.T) {
|
|
err := workflowStore.Resolve(ctx, "t1", "fixed", "admin", "127.0.0.1", now)
|
|
if err == nil {
|
|
t.Error("Resolve() with nil db should return error")
|
|
}
|
|
})
|
|
|
|
t.Run("Close returns error", func(t *testing.T) {
|
|
err := workflowStore.Close(ctx, "t1", "confirmed", "admin", "127.0.0.1", now)
|
|
if err == nil {
|
|
t.Error("Close() with nil db should return error")
|
|
}
|
|
})
|
|
}
|