chore(channels): add email_mockey_test.go, gate mockey tests with build tag
Move TestEmailChannel_checkNewEmails, runIdleLoop, and lifecycleCheck into email_mockey_test.go with //go:build mockey so they only run for go test -tags=mockey -gcflags="all=-N -l", avoiding pr.yml changes.
This commit is contained in:
parent
4fa489cb02
commit
3246270f08
2 changed files with 209 additions and 192 deletions
209
pkg/channels/email_mockey_test.go
Normal file
209
pkg/channels/email_mockey_test.go
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
//go:build mockey
|
||||||
|
|
||||||
|
package channels
|
||||||
|
|
||||||
|
// Tests in this file use github.com/bytedance/mockey and require -gcflags="all=-N -l" to run.
|
||||||
|
// Run with: go test -tags=mockey -gcflags="all=-N -l" ./pkg/channels/...
|
||||||
|
// Without -tags=mockey they are not compiled; without -gcflags they may fail due to Mockey.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/mockey"
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-imap/client"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmailChannel_checkNewEmails(t *testing.T) {
|
||||||
|
// check if the current runtime is go1.25.xx
|
||||||
|
if !strings.HasPrefix(runtime.Version(), "go1.25") {
|
||||||
|
// github.com/bytedance/mockey v1.4.4 is supported in go1.25.xx
|
||||||
|
t.Skip("skipping test in non-go1.25.xx environment")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock connect to return mockClient
|
||||||
|
mockey.PatchConvey("checkNewEmails", t, func() {
|
||||||
|
// --------------- mock start ---------------
|
||||||
|
c := &EmailChannel{
|
||||||
|
BaseChannel: &BaseChannel{
|
||||||
|
bus: bus.NewMessageBus(),
|
||||||
|
},
|
||||||
|
config: config.EmailConfig{
|
||||||
|
Enabled: true,
|
||||||
|
AllowFrom: config.FlexibleStringSlice{},
|
||||||
|
},
|
||||||
|
lastUID: 20,
|
||||||
|
}
|
||||||
|
mockClient := &client.Client{}
|
||||||
|
c.imapClient = mockClient
|
||||||
|
// mock login and select to return mockClient
|
||||||
|
mockey.Mock(mockey.GetMethod(mockClient, "Login")).
|
||||||
|
To(func(imapClient *client.Client, username, password string) error {
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
mockey.Mock(mockey.GetMethod(mockClient, "Select")).
|
||||||
|
To(func(imapClient *client.Client, mailbox string, readonly bool) (*imap.MailboxStatus, error) {
|
||||||
|
return &imap.MailboxStatus{
|
||||||
|
UidNext: 20,
|
||||||
|
}, nil
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
mockey.Mock(mockey.GetMethod(mockClient, "UidSearch")).
|
||||||
|
To(func(imapClient *client.Client, criteria *imap.SearchCriteria) ([]uint32, error) {
|
||||||
|
return []uint32{21}, nil
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
mockey.Mock(mockey.GetMethod(mockClient, "UidFetch")).To(
|
||||||
|
func(imapClient *client.Client, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
|
||||||
|
mimeBytes := []byte(
|
||||||
|
"From: a@b.com\r\nTo: c@d.com\r\nSubject: Test\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nHello world",
|
||||||
|
)
|
||||||
|
section := &imap.BodySectionName{}
|
||||||
|
msg := &imap.Message{
|
||||||
|
Uid: 1,
|
||||||
|
Envelope: &imap.Envelope{Subject: "Test"},
|
||||||
|
Body: map[*imap.BodySectionName]imap.Literal{section: bytes.NewReader(mimeBytes)},
|
||||||
|
}
|
||||||
|
ch <- msg
|
||||||
|
close(ch)
|
||||||
|
return nil
|
||||||
|
}).Build()
|
||||||
|
mockClient.SetState(imap.SelectedState, &imap.MailboxStatus{
|
||||||
|
UidNext: 20,
|
||||||
|
})
|
||||||
|
mockey.Mock(mockey.GetMethod(mockClient, "State")).To(func(*client.Client) imap.ConnState {
|
||||||
|
return imap.SelectedState
|
||||||
|
}).Build()
|
||||||
|
mockey.Mock(mockey.GetMethod(mockClient, "UidStore")).To(
|
||||||
|
func(imapClient *client.Client, seqset *imap.SeqSet, item imap.StoreItem, value any,
|
||||||
|
ch chan *imap.Message,
|
||||||
|
) error {
|
||||||
|
return nil
|
||||||
|
}).Build()
|
||||||
|
// --------------- mock end ---------------
|
||||||
|
ctx := context.Background()
|
||||||
|
c.CheckNewEmails(context.Background())
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
|
||||||
|
defer cancel()
|
||||||
|
messge, ok := c.bus.ConsumeInbound(timeoutCtx)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.True(t, strings.Contains(messge.Content, "Hello world"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmailChannel_runIdleLoop(t *testing.T) {
|
||||||
|
// check if the current runtime is go1.25.xx
|
||||||
|
if !strings.HasPrefix(runtime.Version(), "go1.25") {
|
||||||
|
// github.com/bytedance/mockey v1.4.4 is supported in go1.25.xx
|
||||||
|
t.Skip("skipping test in non-go1.25.xx environment")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock connect to return mockClient
|
||||||
|
mockey.PatchConvey("runIdleLoop", t, func() {
|
||||||
|
// --------------- mock start ---------------
|
||||||
|
hasCheckEmail := false
|
||||||
|
c := &EmailChannel{
|
||||||
|
BaseChannel: &BaseChannel{
|
||||||
|
bus: bus.NewMessageBus(),
|
||||||
|
},
|
||||||
|
config: config.EmailConfig{
|
||||||
|
Enabled: true,
|
||||||
|
AllowFrom: config.FlexibleStringSlice{},
|
||||||
|
},
|
||||||
|
lastUID: 20,
|
||||||
|
}
|
||||||
|
mockClient := &client.Client{}
|
||||||
|
c.imapClient = mockClient
|
||||||
|
// mock login and select to return mockClient
|
||||||
|
mockey.Mock(mockey.GetMethod(c, "CheckNewEmails")).To(func(*EmailChannel, context.Context) {
|
||||||
|
hasCheckEmail = true
|
||||||
|
}).Build()
|
||||||
|
|
||||||
|
mockey.Mock(mockey.GetMethod(mockClient, "State")).To(func(*client.Client) imap.ConnState {
|
||||||
|
return imap.SelectedState
|
||||||
|
}).Build()
|
||||||
|
triggerChannel := make(chan struct{}, 1)
|
||||||
|
mockey.Mock(mockey.GetMethod(mockClient, "Idle")).
|
||||||
|
To(func(self *client.Client, stop <-chan struct{}, opts *client.IdleOptions) error {
|
||||||
|
<-triggerChannel
|
||||||
|
self.Updates <- &client.StatusUpdate{}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
// --------------- mock end ---------------
|
||||||
|
ctx := context.Background()
|
||||||
|
go c.runIdleLoop(ctx, 2*time.Second)
|
||||||
|
assert.False(t, hasCheckEmail)
|
||||||
|
// sent update sigal
|
||||||
|
triggerChannel <- struct{}{}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
assert.True(t, hasCheckEmail)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmailChannel_lifecycleCheck(t *testing.T) {
|
||||||
|
// check if the current runtime is go1.25.xx
|
||||||
|
if !strings.HasPrefix(runtime.Version(), "go1.25") {
|
||||||
|
// github.com/bytedance/mockey v1.4.4 is supported in go1.25.xx
|
||||||
|
t.Skip("skipping test in non-go1.25.xx environment")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mockey.PatchConvey("lifecycle test", t, func() {
|
||||||
|
// --------------- mock start ---------------
|
||||||
|
c := &EmailChannel{
|
||||||
|
BaseChannel: &BaseChannel{
|
||||||
|
bus: bus.NewMessageBus(),
|
||||||
|
},
|
||||||
|
config: config.EmailConfig{
|
||||||
|
Enabled: true,
|
||||||
|
CheckInterval: 1,
|
||||||
|
ForcedPolling: true,
|
||||||
|
IMAPServer: "imap.example.com",
|
||||||
|
Username: "testuser",
|
||||||
|
Password: "testpassword",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// mock login and select to return mockClient
|
||||||
|
mockey.Mock(mockey.GetMethod(c, "connect")).To(func(*EmailChannel) error {
|
||||||
|
return nil
|
||||||
|
}).Build()
|
||||||
|
mockey.Mock(mockey.GetMethod(c, "CheckNewEmails")).To(func(*EmailChannel, context.Context) {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}).Build()
|
||||||
|
// --------------- mock end ---------------
|
||||||
|
ctx := context.Background()
|
||||||
|
err := c.Start(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
var stopDone time.Time
|
||||||
|
stopStart := time.Now()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
c.Stop(ctx)
|
||||||
|
stopDone = time.Now()
|
||||||
|
}()
|
||||||
|
// wait for checkNewEmails to finish
|
||||||
|
assert.True(t, c.IsRunning())
|
||||||
|
wg.Wait()
|
||||||
|
elapsed := stopDone.Sub(stopStart)
|
||||||
|
// stop exit normally
|
||||||
|
assert.False(t, c.IsRunning())
|
||||||
|
// If Stop() did not wait for checkLoop, it would return in milliseconds.
|
||||||
|
assert.GreaterOrEqual(t, elapsed, 1*time.Second, "Stop() must wait for checkLoop (lifecycle compliance)")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -2,20 +2,12 @@ package channels
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bytedance/mockey"
|
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
"github.com/emersion/go-imap/client"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -264,187 +256,3 @@ func Test_saveAttachmentToLocal(t *testing.T) {
|
||||||
path = c.saveAttachmentToLocal(1, 1, "test.txt", body)
|
path = c.saveAttachmentToLocal(1, 1, "test.txt", body)
|
||||||
assert.Empty(t, path)
|
assert.Empty(t, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmailChannel_checkNewEmails(t *testing.T) {
|
|
||||||
// check if the current runtime is go1.25.xx
|
|
||||||
if !strings.HasPrefix(runtime.Version(), "go1.25") {
|
|
||||||
// github.com/bytedance/mockey v1.4.4 is supported in go1.25.xx
|
|
||||||
t.Skip("skipping test in non-go1.25.xx environment")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock connect to return mockClient
|
|
||||||
mockey.PatchConvey("checkNewEmails", t, func() {
|
|
||||||
// --------------- mock start ---------------
|
|
||||||
c := &EmailChannel{
|
|
||||||
BaseChannel: &BaseChannel{
|
|
||||||
bus: bus.NewMessageBus(),
|
|
||||||
},
|
|
||||||
config: config.EmailConfig{
|
|
||||||
Enabled: true,
|
|
||||||
AllowFrom: config.FlexibleStringSlice{},
|
|
||||||
},
|
|
||||||
lastUID: 20,
|
|
||||||
}
|
|
||||||
mockClient := &client.Client{}
|
|
||||||
c.imapClient = mockClient
|
|
||||||
// mock login and select to return mockClient
|
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "Login")).
|
|
||||||
To(func(imapClient *client.Client, username, password string) error {
|
|
||||||
return nil
|
|
||||||
}).
|
|
||||||
Build()
|
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "Select")).
|
|
||||||
To(func(imapClient *client.Client, mailbox string, readonly bool) (*imap.MailboxStatus, error) {
|
|
||||||
return &imap.MailboxStatus{
|
|
||||||
UidNext: 20,
|
|
||||||
}, nil
|
|
||||||
}).
|
|
||||||
Build()
|
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "UidSearch")).
|
|
||||||
To(func(imapClient *client.Client, criteria *imap.SearchCriteria) ([]uint32, error) {
|
|
||||||
return []uint32{21}, nil
|
|
||||||
}).
|
|
||||||
Build()
|
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "UidFetch")).To(
|
|
||||||
func(imapClient *client.Client, seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
|
|
||||||
mimeBytes := []byte(
|
|
||||||
"From: a@b.com\r\nTo: c@d.com\r\nSubject: Test\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nHello world",
|
|
||||||
)
|
|
||||||
section := &imap.BodySectionName{}
|
|
||||||
msg := &imap.Message{
|
|
||||||
Uid: 1,
|
|
||||||
Envelope: &imap.Envelope{Subject: "Test"},
|
|
||||||
Body: map[*imap.BodySectionName]imap.Literal{section: bytes.NewReader(mimeBytes)},
|
|
||||||
}
|
|
||||||
ch <- msg
|
|
||||||
close(ch)
|
|
||||||
return nil
|
|
||||||
}).Build()
|
|
||||||
mockClient.SetState(imap.SelectedState, &imap.MailboxStatus{
|
|
||||||
UidNext: 20,
|
|
||||||
})
|
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "State")).To(func(*client.Client) imap.ConnState {
|
|
||||||
return imap.SelectedState
|
|
||||||
}).Build()
|
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "UidStore")).To(
|
|
||||||
func(imapClient *client.Client, seqset *imap.SeqSet, item imap.StoreItem, value any,
|
|
||||||
ch chan *imap.Message,
|
|
||||||
) error {
|
|
||||||
return nil
|
|
||||||
}).Build()
|
|
||||||
// --------------- mock end ---------------
|
|
||||||
ctx := context.Background()
|
|
||||||
c.CheckNewEmails(context.Background())
|
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
|
|
||||||
defer cancel()
|
|
||||||
messge, ok := c.bus.ConsumeInbound(timeoutCtx)
|
|
||||||
assert.True(t, ok)
|
|
||||||
assert.True(t, strings.Contains(messge.Content, "Hello world"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmailChannel_runIdleLoop(t *testing.T) {
|
|
||||||
// check if the current runtime is go1.25.xx
|
|
||||||
if !strings.HasPrefix(runtime.Version(), "go1.25") {
|
|
||||||
// github.com/bytedance/mockey v1.4.4 is supported in go1.25.xx
|
|
||||||
t.Skip("skipping test in non-go1.25.xx environment")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock connect to return mockClient
|
|
||||||
mockey.PatchConvey("runIdleLoop", t, func() {
|
|
||||||
// --------------- mock start ---------------
|
|
||||||
hasCheckEmail := false
|
|
||||||
c := &EmailChannel{
|
|
||||||
BaseChannel: &BaseChannel{
|
|
||||||
bus: bus.NewMessageBus(),
|
|
||||||
},
|
|
||||||
config: config.EmailConfig{
|
|
||||||
Enabled: true,
|
|
||||||
AllowFrom: config.FlexibleStringSlice{},
|
|
||||||
},
|
|
||||||
lastUID: 20,
|
|
||||||
}
|
|
||||||
mockClient := &client.Client{}
|
|
||||||
c.imapClient = mockClient
|
|
||||||
// mock login and select to return mockClient
|
|
||||||
mockey.Mock(mockey.GetMethod(c, "CheckNewEmails")).To(func(*EmailChannel, context.Context) {
|
|
||||||
hasCheckEmail = true
|
|
||||||
}).Build()
|
|
||||||
|
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "State")).To(func(*client.Client) imap.ConnState {
|
|
||||||
return imap.SelectedState
|
|
||||||
}).Build()
|
|
||||||
triggerChannel := make(chan struct{}, 1)
|
|
||||||
mockey.Mock(mockey.GetMethod(mockClient, "Idle")).
|
|
||||||
To(func(self *client.Client, stop <-chan struct{}, opts *client.IdleOptions) error {
|
|
||||||
<-triggerChannel
|
|
||||||
self.Updates <- &client.StatusUpdate{}
|
|
||||||
return nil
|
|
||||||
}).
|
|
||||||
Build()
|
|
||||||
// --------------- mock end ---------------
|
|
||||||
ctx := context.Background()
|
|
||||||
go c.runIdleLoop(ctx, 2*time.Second)
|
|
||||||
assert.False(t, hasCheckEmail)
|
|
||||||
// sent update sigal
|
|
||||||
triggerChannel <- struct{}{}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
assert.True(t, hasCheckEmail)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmailChannel_lifecycleCheck(t *testing.T) {
|
|
||||||
// check if the current runtime is go1.25.xx
|
|
||||||
if !strings.HasPrefix(runtime.Version(), "go1.25") {
|
|
||||||
// github.com/bytedance/mockey v1.4.4 is supported in go1.25.xx
|
|
||||||
t.Skip("skipping test in non-go1.25.xx environment")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mockey.PatchConvey("lifecycle test", t, func() {
|
|
||||||
// --------------- mock start ---------------
|
|
||||||
c := &EmailChannel{
|
|
||||||
BaseChannel: &BaseChannel{
|
|
||||||
bus: bus.NewMessageBus(),
|
|
||||||
},
|
|
||||||
config: config.EmailConfig{
|
|
||||||
Enabled: true,
|
|
||||||
CheckInterval: 1,
|
|
||||||
ForcedPolling: true,
|
|
||||||
IMAPServer: "imap.example.com",
|
|
||||||
Username: "testuser",
|
|
||||||
Password: "testpassword",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
// mock login and select to return mockClient
|
|
||||||
mockey.Mock(mockey.GetMethod(c, "connect")).To(func(*EmailChannel) error {
|
|
||||||
return nil
|
|
||||||
}).Build()
|
|
||||||
mockey.Mock(mockey.GetMethod(c, "CheckNewEmails")).To(func(*EmailChannel, context.Context) {
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}).Build()
|
|
||||||
// --------------- mock end ---------------
|
|
||||||
ctx := context.Background()
|
|
||||||
err := c.Start(ctx)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(1)
|
|
||||||
var stopDone time.Time
|
|
||||||
stopStart := time.Now()
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
c.Stop(ctx)
|
|
||||||
stopDone = time.Now()
|
|
||||||
}()
|
|
||||||
// wait for checkNewEmails to finish
|
|
||||||
assert.True(t, c.IsRunning())
|
|
||||||
wg.Wait()
|
|
||||||
elapsed := stopDone.Sub(stopStart)
|
|
||||||
// stop exit normally
|
|
||||||
assert.False(t, c.IsRunning())
|
|
||||||
// If Stop() did not wait for checkLoop, it would return in milliseconds.
|
|
||||||
assert.GreaterOrEqual(t, elapsed, 1*time.Second, "Stop() must wait for checkLoop (lifecycle compliance)")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue