cmc-sales/go/internal/cmc/handlers/quotes/quotes_test.go

360 lines
14 KiB
Go

package handlers
import (
"context"
"database/sql"
"errors"
"testing"
"time"
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
)
// Mocks
type mockQuoteRow struct {
reminderType int32
reminderSent time.Time
designatedDay time.Weekday
emailSent bool
}
func (m *mockQuoteRow) GetReminderType() int32 { return m.reminderType }
func (m *mockQuoteRow) GetReminderSent() time.Time { return m.reminderSent }
func (m *mockQuoteRow) GetCustomerEmail() string { return "test@example.com" }
// Realistic mock for db.Queries
type mockQueries struct {
reminders []db.QuoteReminder
insertCalled bool
}
func (m *mockQueries) GetQuoteRemindersByType(ctx context.Context, params db.GetQuoteRemindersByTypeParams) ([]db.QuoteReminder, error) {
var filtered []db.QuoteReminder
for _, r := range m.reminders {
if r.QuoteID == params.QuoteID && r.ReminderType == params.ReminderType {
filtered = append(filtered, r)
}
}
return filtered, nil
}
func (m *mockQueries) InsertQuoteReminder(ctx context.Context, params db.InsertQuoteReminderParams) (sql.Result, error) {
m.insertCalled = true
m.reminders = append(m.reminders, db.QuoteReminder{
QuoteID: params.QuoteID,
ReminderType: params.ReminderType,
DateSent: params.DateSent,
Username: params.Username,
})
return &mockResult{}, nil
}
func (m *mockQueries) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesRow, error) {
return nil, nil
}
func (m *mockQueries) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesRow, error) {
return nil, nil
}
func (m *mockQueries) GetExpiringSoonQuotesOnDay(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesOnDayRow, error) {
return nil, nil
}
func (m *mockQueries) GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesOnDayRow, error) {
return nil, nil
}
// Mock sql.Result for InsertQuoteReminder
type mockResult struct{}
func (m *mockResult) LastInsertId() (int64, error) { return 1, nil }
func (m *mockResult) RowsAffected() (int64, error) { return 1, nil }
// Realistic mock for email.EmailService
type mockEmailService struct {
sent bool
sentReminders map[int32]map[int32]bool // quoteID -> reminderType -> sent
}
func (m *mockEmailService) SendTemplateEmail(to, subject, templateName string, data interface{}, ccs, bccs []string) error {
m.sent = true
if m.sentReminders == nil {
m.sentReminders = make(map[int32]map[int32]bool)
}
var quoteID int32
var reminderType int32
if dataMap, ok := data.(map[string]interface{}); ok {
if id, ok := dataMap["QuoteID"].(int32); ok {
quoteID = id
} else if id, ok := dataMap["QuoteID"].(int); ok {
quoteID = int32(id)
}
if rt, ok := dataMap["ReminderType"].(int32); ok {
reminderType = rt
} else if rt, ok := dataMap["ReminderType"].(int); ok {
reminderType = int32(rt)
}
}
if quoteID == 0 {
quoteID = 123
}
if reminderType == 0 {
reminderType = 1
}
if m.sentReminders[quoteID] == nil {
m.sentReminders[quoteID] = make(map[int32]bool)
}
m.sentReminders[quoteID][reminderType] = true
return nil
}
// Mock for db.Queries with error simulation
type mockQueriesError struct{}
func (m *mockQueriesError) GetQuoteRemindersByType(ctx context.Context, params db.GetQuoteRemindersByTypeParams) ([]db.QuoteReminder, error) {
return nil, errors.New("db error")
}
func (m *mockQueriesError) InsertQuoteReminder(ctx context.Context, params db.InsertQuoteReminderParams) (sql.Result, error) {
return &mockResult{}, nil
}
func (m *mockQueriesError) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesRow, error) {
return nil, nil
}
func (m *mockQueriesError) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesRow, error) {
return nil, nil
}
func (m *mockQueriesError) GetExpiringSoonQuotesOnDay(ctx context.Context, dateADD interface{}) ([]db.GetExpiringSoonQuotesOnDayRow, error) {
return nil, nil
}
func (m *mockQueriesError) GetRecentlyExpiredQuotesOnDay(ctx context.Context, dateSUB interface{}) ([]db.GetRecentlyExpiredQuotesOnDayRow, error) {
return nil, nil
}
// Test: Should send email on designated day if not already sent
// Description: Verifies that a reminder email is sent and recorded when no previous reminder exists for the quote. Should pass unless the handler logic is broken.
func TestSendQuoteReminderEmail_OnDesignatedDay(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{}}
me := &mockEmailService{}
h := &QuotesHandler{
queries: mq,
emailService: me,
}
// Simulate designated day logic by calling SendQuoteReminderEmail
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !me.sent {
t.Error("Expected email to be sent on designated day")
}
if !mq.insertCalled {
t.Error("Expected reminder to be recorded on designated day")
}
}
// Test: Should NOT send email if reminder already sent
// Description: Verifies that if a reminder for the quote and type already exists, the handler does not send another email or record a duplicate. Should fail if duplicate reminders are allowed.
func TestSendQuoteReminderEmail_AlreadyReminded(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{{QuoteID: 123, ReminderType: 1}}}
me := &mockEmailService{}
h := &QuotesHandler{
queries: mq,
emailService: me,
}
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil)
if err == nil {
t.Error("Expected error for already sent reminder")
}
if me.sent {
t.Error("Expected email NOT to be sent if already reminded")
}
if mq.insertCalled {
t.Error("Expected reminder NOT to be recorded if already reminded")
}
}
// Test: Should only send reminder once, second call should fail
// Description: Sends a reminder, then tries to send the same reminder again. The first should succeed, the second should fail and not send or record a duplicate. Should fail if duplicates are allowed.
func TestSendQuoteReminderEmail_OnlyOnce(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{}}
me := &mockEmailService{}
h := &QuotesHandler{
queries: mq,
emailService: me,
}
// First call should succeed
err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil)
if err1 != nil {
t.Fatalf("Expected first call to succeed, got %v", err1)
}
if !me.sentReminders[123][1] {
t.Error("Expected email to be sent and recorded for quote 123, reminder 1")
}
if len(mq.reminders) != 1 {
t.Errorf("Expected 1 reminder recorded in DB, got %d", len(mq.reminders))
}
// Second call should fail
err2 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil)
if err2 == nil {
t.Error("Expected error for already sent reminder on second call")
}
if len(mq.reminders) != 1 {
t.Errorf("Expected no additional reminder recorded in DB, got %d", len(mq.reminders))
}
}
// Test: Should send and record reminder if not already sent
// Description: Verifies that a reminder is sent and recorded when no previous reminder exists. Should pass unless the handler logic is broken.
func TestSendQuoteReminderEmail_SendsIfNotAlreadySent(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{}}
me := &mockEmailService{}
h := &QuotesHandler{
queries: mq,
emailService: me,
}
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !me.sent {
t.Error("Expected email to be sent")
}
if !mq.insertCalled {
t.Error("Expected reminder to be recorded")
}
}
// Test: Should ignore already sent reminder
// Description: Verifies that the handler does not send or record a reminder if one already exists for the quote/type. Should fail if duplicates are allowed.
func TestSendQuoteReminderEmail_IgnoresIfAlreadySent(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{{QuoteID: 123, ReminderType: 1}}}
me := &mockEmailService{}
h := &QuotesHandler{
queries: mq,
emailService: me,
}
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil)
if err == nil {
t.Error("Expected error for already sent reminder")
}
if me.sent {
t.Error("Expected email NOT to be sent")
}
if mq.insertCalled {
t.Error("Expected reminder NOT to be recorded")
}
}
// Test: Should fail if DB returns error
// Description: Simulates a DB error and expects the handler to return an error. Should fail if DB errors are not handled.
func TestSendQuoteReminderEmail_DBError(t *testing.T) {
h := &QuotesHandler{queries: &mockQueriesError{}}
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{}, nil)
if err == nil {
t.Error("Expected error when DB fails, got nil")
}
}
// Edge case: nil recipient
// Test: Should fail if recipient is empty
// Description: Verifies that the handler returns an error if the recipient email is missing. Should fail if emails are sent to empty recipients.
func TestSendQuoteReminderEmail_NilRecipient(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{}}
h := &QuotesHandler{queries: mq}
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "", "Subject", "template", map[string]interface{}{}, nil)
if err == nil {
t.Error("Expected error for nil recipient, got nil")
}
}
// Edge case: missing template data
// Test: Should fail if template data is missing
// Description: Verifies that the handler returns an error if template data is nil. Should fail if emails are sent without template data.
func TestSendQuoteReminderEmail_MissingTemplateData(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{}}
h := &QuotesHandler{queries: mq}
err := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", nil, nil)
if err == nil {
t.Error("Expected error for missing template data, got nil")
}
}
// Boundary: invalid reminder type
// Test: Should fail if reminder type is invalid
// Description: Verifies that the handler returns an error for an invalid reminder type. Should fail if invalid types are allowed.
func TestSendQuoteReminderEmail_InvalidReminderType(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{}}
h := &QuotesHandler{queries: mq}
err := h.SendQuoteReminderEmail(context.Background(), 123, 99, "test@example.com", "Subject", "template", map[string]interface{}{}, nil)
if err == nil {
t.Error("Expected error for invalid reminder type, got nil")
}
}
// Test: Multiple reminders of different types allowed for same quote
// Description: Verifies that reminders of different types for the same quote can be sent and recorded independently. Should fail if only one reminder per quote is allowed.
func TestSendQuoteReminderEmail_MultipleTypes(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{}}
me := &mockEmailService{}
h := &QuotesHandler{queries: mq, emailService: me}
// First reminder type
err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil)
if err1 != nil {
t.Fatalf("Expected first reminder to be sent, got %v", err1)
}
if !me.sentReminders[123][1] {
t.Error("Expected email to be sent and recorded for quote 123, reminder 1")
}
if len(mq.reminders) != 1 {
t.Errorf("Expected 1 reminder recorded in DB after first send, got %d", len(mq.reminders))
}
// Second reminder type
err2 := h.SendQuoteReminderEmail(context.Background(), 123, 2, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 2}, nil)
if err2 != nil {
t.Fatalf("Expected second reminder to be sent, got %v", err2)
}
if !me.sentReminders[123][2] {
t.Error("Expected email to be sent and recorded for quote 123, reminder 2")
}
if len(mq.reminders) != 2 {
t.Errorf("Expected 2 reminders recorded in DB after both sends, got %d", len(mq.reminders))
}
}
// Test: Reminders for different quotes are independent
// Description: Verifies that reminders for different quotes do not block each other and are recorded independently. Should fail if reminders for one quote affect another.
func TestSendQuoteReminderEmail_DifferentQuotes(t *testing.T) {
mq := &mockQueries{reminders: []db.QuoteReminder{}}
me := &mockEmailService{}
h := &QuotesHandler{queries: mq, emailService: me}
// Send reminder for quote 123
err1 := h.SendQuoteReminderEmail(context.Background(), 123, 1, "test@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 123, "ReminderType": 1}, nil)
if err1 != nil {
t.Fatalf("Expected reminder for quote 123 to be sent, got %v", err1)
}
if !me.sentReminders[123][1] {
t.Error("Expected email to be sent and recorded for quote 123, reminder 1")
}
if len(mq.reminders) != 1 {
t.Errorf("Expected 1 reminder recorded in DB after first send, got %d", len(mq.reminders))
}
// Send reminder for quote 456
err2 := h.SendQuoteReminderEmail(context.Background(), 456, 1, "test2@example.com", "Subject", "template", map[string]interface{}{"QuoteID": 456, "ReminderType": 1}, nil)
if err2 != nil {
t.Fatalf("Expected reminder for quote 456 to be sent, got %v", err2)
}
if !me.sentReminders[456][1] {
t.Error("Expected email to be sent and recorded for quote 456, reminder 1")
}
if len(mq.reminders) != 2 {
t.Errorf("Expected 2 reminders recorded in DB after both sends, got %d", len(mq.reminders))
}
}