From 810f777e38945271eee2ccbbc0255158d7c59be0 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Tue, 30 Dec 2025 11:39:03 -0500 Subject: [PATCH] Add a test for the main message moving code --- cmd/mailpopbox-router/monitor.go | 36 ++--- cmd/mailpopbox-router/monitor_test.go | 225 ++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 18 deletions(-) create mode 100644 cmd/mailpopbox-router/monitor_test.go diff --git a/cmd/mailpopbox-router/monitor.go b/cmd/mailpopbox-router/monitor.go index b1af1ea..3b4a318 100644 --- a/cmd/mailpopbox-router/monitor.go +++ b/cmd/mailpopbox-router/monitor.go @@ -16,59 +16,59 @@ import ( ) type Monitor struct { - c MonitorConfig - auth OAuthServer - log *zap.Logger + c MonitorConfig + log *zap.Logger + + src Source + dst Destination } func NewMontior(config MonitorConfig, auth OAuthServer, log *zap.Logger) *Monitor { log = log.With(zap.String("source", config.Source.LogDescription()), zap.String("dest", config.Destination.LogDescription())) return &Monitor{ - c: config, - auth: auth, - log: log, + c: config, + log: log, + src: NewSource(config.Source, auth, log), + dst: NewDestination(config.Destination, auth, log), } } func (m *Monitor) Start(ctx context.Context) error { - src := NewSource(m.c.Source, m.auth, m.log) - dst := NewDestination(m.c.Destination, m.auth, m.log) - - if err := m.runOnce(ctx, src, dst); err != nil { + if err := m.runOnce(ctx); err != nil { m.log.Error("Failed to start monitor", zap.Error(err)) return err } - go m.run(ctx, src, dst) + go m.run(ctx) return nil } -func (m *Monitor) run(ctx context.Context, src Source, dst Destination) { +func (m *Monitor) run(ctx context.Context) { for { select { case <-ctx.Done(): m.log.Info("Monitor stopping") return case <-time.After(m.c.PollIntervalSeconds * time.Second): - m.runOnce(ctx, src, dst) + m.runOnce(ctx) } } } -func (m *Monitor) runOnce(ctx context.Context, src Source, dst Destination) error { +func (m *Monitor) runOnce(ctx context.Context) error { m.log.Info("Polling for messages") - if err := src.Connect(); err != nil { + if err := m.src.Connect(); err != nil { return fmt.Errorf("Failed to connect to source: %w", err) } - dstConn, err := dst.Connect(ctx) + dstConn, err := m.dst.Connect(ctx) if err != nil { return fmt.Errorf("Failed to connect to dest: %w", err) } - msgs, err := src.GetMessages() + msgs, err := m.src.GetMessages() if err != nil { return fmt.Errorf("Failed to list messages: %w", err) } @@ -83,7 +83,7 @@ func (m *Monitor) runOnce(ctx context.Context, src Source, dst Destination) erro } } - if err := src.Close(); err != nil { + if err := m.src.Close(); err != nil { return fmt.Errorf("Failed to close source: %w", err) } if err := dstConn.Close(); err != nil { diff --git a/cmd/mailpopbox-router/monitor_test.go b/cmd/mailpopbox-router/monitor_test.go new file mode 100644 index 0000000..e893d3c --- /dev/null +++ b/cmd/mailpopbox-router/monitor_test.go @@ -0,0 +1,225 @@ +// mailpopbox +// Copyright 2025 Blue Static +// This program is free software licensed under the GNU General Public License, +// version 3.0. The full text of the license can be found in LICENSE.txt. +// SPDX-License-Identifier: GPL-3.0-only + +package main + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "testing" + "time" + + "go.uber.org/zap" +) + +type testSource struct { + connectErr error + getMsgs func() ([]Message, error) +} + +func (s *testSource) Connect() error { return s.connectErr } +func (s *testSource) Reset() error { return nil } +func (s *testSource) Close() error { return nil } +func (s *testSource) GetMessages() ([]Message, error) { + return s.getMsgs() +} + +type testMessage struct { + id string + buf bytes.Buffer + contentErr error + deleted bool + deleteErr error +} + +func (m *testMessage) ID() string { return m.id } +func (m *testMessage) Content() (io.ReadCloser, error) { + if m.contentErr != nil { + return nil, m.contentErr + } + return io.NopCloser(&m.buf), nil +} +func (m *testMessage) Delete() error { + m.deleted = true + return m.deleteErr +} + +type testDestination struct { + connectErr error + msgs [][]byte + addMsgErr error + closeErr error +} + +func (d *testDestination) Connect(context.Context) (DestinationConnection, error) { + if d.connectErr != nil { + return nil, d.connectErr + } + return d, nil +} +func (d *testDestination) AddMessage(msg []byte) error { + if d.addMsgErr == nil { + d.msgs = append(d.msgs, msg) + } + return d.addMsgErr +} +func (d *testDestination) Close() error { + return d.closeErr +} + +func makeMonitor(src Source, dst Destination) *Monitor { + return &Monitor{ + c: MonitorConfig{PollIntervalSeconds: 1 * time.Hour}, + log: zap.L(), + src: src, + dst: dst, + } +} + +var ( + srcConnErr = fmt.Errorf("source-connect-err") + dstConnErr = fmt.Errorf("dest-connect-err") + getMsgsErr = fmt.Errorf("get-msgs") + getMsgContentErr = fmt.Errorf("get-msg-content") + addMsgErr = fmt.Errorf("add-msg") + msgDeleteErr = fmt.Errorf("delete-msg") +) + +func TestSourceConnectError(t *testing.T) { + s := &testSource{connectErr: srcConnErr} + d := &testDestination{} + m := makeMonitor(s, d) + err := m.Start(t.Context()) + if err == nil { + t.Errorf("Expected error in Start, got nil") + } else if !errors.Is(err, srcConnErr) { + t.Errorf("Error is not %v", srcConnErr) + } +} + +func TestDestConnectError(t *testing.T) { + s := &testSource{} + d := &testDestination{connectErr: dstConnErr} + m := makeMonitor(s, d) + err := m.Start(t.Context()) + if err == nil { + t.Errorf("Expected error in Start, got nil") + } else if !errors.Is(err, dstConnErr) { + t.Errorf("Error is not %v", dstConnErr) + } +} + +func TestGetMessagesError(t *testing.T) { + s := &testSource{ + getMsgs: func() ([]Message, error) { + return nil, getMsgsErr + }, + } + d := &testDestination{} + m := makeMonitor(s, d) + err := m.Start(t.Context()) + if err == nil { + t.Errorf("Expected error in Start, got nil") + } else if !errors.Is(err, getMsgsErr) { + t.Errorf("Error is not %v", getMsgsErr) + } +} + +func TestMoveOneMessageSuccess(t *testing.T) { + msg := &testMessage{id: "msg1"} + fmt.Fprintln(&msg.buf, "Message1") + s := &testSource{ + getMsgs: func() ([]Message, error) { + return []Message{msg}, nil + }, + } + d := &testDestination{} + m := makeMonitor(s, d) + err := m.Start(t.Context()) + if err != nil { + t.Errorf("Expected monitor to Start successfully") + } + if !msg.deleted { + t.Errorf("Expected source message to be deleted") + } + if want, got := 1, len(d.msgs); want != got { + t.Errorf("Expected %d dest messages, got %d", want, got) + } + if !bytes.HasSuffix(d.msgs[0], msg.buf.Bytes()) { + t.Errorf("Expected dest message to contain %s, got %s", string(msg.buf.Bytes()), string(d.msgs[0])) + } +} + +func TestMoveMessageFailRead(t *testing.T) { + msg := &testMessage{id: "msg1", contentErr: getMsgContentErr} + s := &testSource{ + getMsgs: func() ([]Message, error) { + return []Message{msg}, nil + }, + } + d := &testDestination{} + m := makeMonitor(s, d) + err := m.Start(t.Context()) + if err != nil { + t.Errorf("Expected monitor to Start successfully") + } + if msg.deleted { + t.Errorf("Expected source message to remain") + } + if want, got := 0, len(d.msgs); want != got { + t.Errorf("Expected %d dest messages, got %d", want, got) + } +} + +func TestMoveMessageFailWrite(t *testing.T) { + msg := &testMessage{id: "msg1"} + fmt.Fprintln(&msg.buf, "Message1") + s := &testSource{ + getMsgs: func() ([]Message, error) { + return []Message{msg}, nil + }, + } + d := &testDestination{addMsgErr: addMsgErr} + m := makeMonitor(s, d) + err := m.Start(t.Context()) + if err != nil { + t.Errorf("Expected monitor to Start successfully") + } + if msg.deleted { + t.Errorf("Expected source message to remain") + } + if want, got := 0, len(d.msgs); want != got { + t.Errorf("Expected %d dest messages, got %d", want, got) + } +} + +func TestMoveOneMessageDeleteError(t *testing.T) { + msg := &testMessage{id: "msg1", deleteErr: msgDeleteErr} + fmt.Fprintln(&msg.buf, "Message1") + s := &testSource{ + getMsgs: func() ([]Message, error) { + return []Message{msg}, nil + }, + } + d := &testDestination{} + m := makeMonitor(s, d) + err := m.Start(t.Context()) + if err != nil { + t.Errorf("Expected monitor to Start successfully") + } + if !msg.deleted { + t.Errorf("Expected source message to be deleted") + } + if want, got := 1, len(d.msgs); want != got { + t.Errorf("Expected %d dest messages, got %d", want, got) + } + if !bytes.HasSuffix(d.msgs[0], msg.buf.Bytes()) { + t.Errorf("Expected dest message to contain %s, got %s", string(msg.buf.Bytes()), string(d.msgs[0])) + } +} -- 2.43.5