package app import ( "errors" "sync" "testing" "time" "it2000.com.cn/tele-recv/config" "it2000.com.cn/tele-recv/storage" ) // mockSender implements transport.Sender for testing. type mockSender struct { mu sync.Mutex sent []string sendErr error closeErr error } func (m *mockSender) Send(telegram string) error { m.mu.Lock() defer m.mu.Unlock() if m.sendErr != nil { return m.sendErr } m.sent = append(m.sent, telegram) return nil } func (m *mockSender) Close() error { return m.closeErr } // mockStore implements storage insertion for testing. type mockStore struct { mu sync.Mutex telegrams []string insertErr error } func (m *mockStore) Insert(telegram string) error { m.mu.Lock() defer m.mu.Unlock() if m.insertErr != nil { return m.insertErr } m.telegrams = append(m.telegrams, telegram) return nil } func (m *mockStore) LoadUnprocessed() ([]storage.Telegram, error) { return nil, nil } func (m *mockStore) MarkProcessed(id int64) error { return nil } func (m *mockStore) Close() error { return nil } func TestNewApp_NilConfig(t *testing.T) { // Should not panic, but serial port open will fail later cfg := &config.Config{ Serial: config.SerialConfig{ Device: "/nonexistent", Baudrate: 9600, }, SQLite: config.SQLiteConfig{ File: ":memory:", Init: true, }, Log: config.LogConfig{ Dir: t.TempDir(), MaxAge: 1, RotateHour: 24, }, } a, err := New(cfg) if err != nil { t.Fatalf("New() failed: %v", err) } if a == nil { t.Fatal("expected non-nil app") } } func TestDispatch_StoreAndSend(t *testing.T) { store := &mockStore{} sender := &mockSender{} cfg := &config.Config{ SQLite: config.SQLiteConfig{File: ":memory:"}, Log: config.LogConfig{Dir: t.TempDir(), MaxAge: 1, RotateHour: 24}, } app, err := New(cfg) if err != nil { t.Fatalf("New() failed: %v", err) } app.store = store app.sender = sender app.dispatch("ZCZC TEST NNNN") if len(store.telegrams) != 1 { t.Errorf("expected 1 stored telegram, got %d", len(store.telegrams)) } if len(sender.sent) != 1 { t.Errorf("expected 1 sent telegram, got %d", len(sender.sent)) } } func TestDispatch_StoreError(t *testing.T) { store := &mockStore{insertErr: errors.New("db error")} sender := &mockSender{} cfg := &config.Config{ SQLite: config.SQLiteConfig{File: ":memory:"}, Log: config.LogConfig{Dir: t.TempDir(), MaxAge: 1, RotateHour: 24}, } app, err := New(cfg) if err != nil { t.Fatalf("New() failed: %v", err) } app.store = store app.sender = sender // Should not panic on store error, should still send app.dispatch("ZCZC TEST NNNN") if len(sender.sent) != 1 { t.Errorf("expected 1 sent telegram despite store error, got %d", len(sender.sent)) } } func TestDispatch_SendError(t *testing.T) { store := &mockStore{} sender := &mockSender{sendErr: errors.New("send error")} cfg := &config.Config{ SQLite: config.SQLiteConfig{File: ":memory:"}, Log: config.LogConfig{Dir: t.TempDir(), MaxAge: 1, RotateHour: 24}, } app, err := New(cfg) if err != nil { t.Fatalf("New() failed: %v", err) } app.store = store app.sender = sender // Should not panic on send error, should still store app.dispatch("ZCZC TEST NNNN") if len(store.telegrams) != 1 { t.Errorf("expected 1 stored telegram despite send error, got %d", len(store.telegrams)) } } func TestRun_CancelledContext(t *testing.T) { cfg := &config.Config{ Serial: config.SerialConfig{ Device: "/nonexistent", Baudrate: 9600, }, SQLite: config.SQLiteConfig{File: ":memory:"}, Log: config.LogConfig{Dir: t.TempDir(), MaxAge: 1, RotateHour: 24}, } app, err := New(cfg) if err != nil { t.Fatalf("New() failed: %v", err) } // Cancel after a short delay so Run() tries opening port and exits cleanly go func() { time.Sleep(50 * time.Millisecond) app.cancel() }() err = app.Run() if err != nil { t.Fatalf("Run() should return nil on cancellation, got: %v", err) } }