2025-07-19 23:50:09 -07:00
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
2025-12-03 05:35:03 -08:00
err := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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 ,
}
2025-12-03 05:35:03 -08:00
err := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { "QuoteID" : 123 , "ReminderType" : 1 } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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
2025-12-03 05:35:03 -08:00
err1 := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { "QuoteID" : 123 , "ReminderType" : 1 } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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
2025-12-03 05:35:03 -08:00
err2 := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { "QuoteID" : 123 , "ReminderType" : 1 } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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 ,
}
2025-12-03 05:35:03 -08:00
err := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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 ,
}
2025-12-03 05:35:03 -08:00
err := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { "QuoteID" : 123 , "ReminderType" : 1 } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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 { } }
2025-12-03 05:35:03 -08:00
err := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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 }
2025-12-03 05:35:03 -08:00
err := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "" , "Subject" , "template" , map [ string ] interface { } { } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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 }
2025-12-03 05:35:03 -08:00
err := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , nil , nil , nil , false )
2025-07-19 23:50:09 -07:00
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 }
2025-12-03 05:35:03 -08:00
err := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 99 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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
2025-12-03 05:35:03 -08:00
err1 := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { "QuoteID" : 123 , "ReminderType" : 1 } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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
2025-12-03 05:35:03 -08:00
err2 := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 2 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { "QuoteID" : 123 , "ReminderType" : 2 } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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
2025-12-03 05:35:03 -08:00
err1 := h . SendQuoteReminderEmail ( context . Background ( ) , 123 , 1 , "test@example.com" , "Subject" , "template" , map [ string ] interface { } { "QuoteID" : 123 , "ReminderType" : 1 } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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
2025-12-03 05:35:03 -08:00
err2 := h . SendQuoteReminderEmail ( context . Background ( ) , 456 , 1 , "test2@example.com" , "Subject" , "template" , map [ string ] interface { } { "QuoteID" : 456 , "ReminderType" : 1 } , nil , nil , false )
2025-07-19 23:50:09 -07:00
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 ) )
}
}