package test import ( "RenewCMS/internal/infrastructure/mailer" "bytes" "log" "strings" "sync" "testing" "time" ) type dispatcherLogBuffer struct { mu sync.Mutex buffer bytes.Buffer } func (b *dispatcherLogBuffer) Write(p []byte) (int, error) { b.mu.Lock() defer b.mu.Unlock() return b.buffer.Write(p) } func (b *dispatcherLogBuffer) String() string { b.mu.Lock() defer b.mu.Unlock() return b.buffer.String() } func TestAccountChangeEmailAsyncDispatcherRunsJobInBackground(t *testing.T) { dispatcher := mailer.NewAsyncDispatcher() release := make(chan struct{}) done := make(chan struct{}) dispatcher.Dispatch(func() { <-release close(done) }) select { case <-done: t.Fatal("job completed before it was released, so Dispatch did not return independently") default: } close(release) select { case <-done: case <-time.After(2 * time.Second): t.Fatal("dispatched job never ran") } } func TestAccountChangeEmailAsyncDispatcherRecoversAndLogsPanic(t *testing.T) { buffer := &dispatcherLogBuffer{} previous := log.Writer() log.SetOutput(buffer) defer log.SetOutput(previous) dispatcher := mailer.NewAsyncDispatcher() finished := make(chan struct{}) dispatcher.Dispatch(func() { defer close(finished) panic("boom") }) select { case <-finished: case <-time.After(2 * time.Second): t.Fatal("panicking job never ran") } deadline := time.Now().Add(2 * time.Second) for !strings.Contains(buffer.String(), "boom") && time.Now().Before(deadline) { time.Sleep(5 * time.Millisecond) } if !strings.Contains(buffer.String(), "panic in a background mail job: boom") { t.Fatalf("panic was not logged, got %q", buffer.String()) } followUp := make(chan struct{}) dispatcher.Dispatch(func() { close(followUp) }) select { case <-followUp: case <-time.After(2 * time.Second): t.Fatal("dispatcher unusable after a recovered panic") } }