mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
450 lines
12 KiB
Go
450 lines
12 KiB
Go
package test
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/mail"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/inbucket/inbucket/v3/pkg/config"
|
|
"github.com/inbucket/inbucket/v3/pkg/extension"
|
|
"github.com/inbucket/inbucket/v3/pkg/extension/event"
|
|
"github.com/inbucket/inbucket/v3/pkg/message"
|
|
"github.com/inbucket/inbucket/v3/pkg/storage"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// StoreFactory returns a new store for the test suite.
|
|
type StoreFactory func(
|
|
config.Storage, *extension.Host) (store storage.Store, destroy func(), err error)
|
|
|
|
// suite is passed to each test function; embeds `testing.T` to provide testing primitives.
|
|
type suite struct {
|
|
*testing.T
|
|
store storage.Store
|
|
extHost *extension.Host
|
|
}
|
|
|
|
// StoreSuite runs a set of general tests on the provided Store.
|
|
func StoreSuite(t *testing.T, factory StoreFactory) {
|
|
t.Helper()
|
|
testCases := []struct {
|
|
name string
|
|
test func(suite)
|
|
conf config.Storage
|
|
}{
|
|
{"metadata", testMetadata, config.Storage{}},
|
|
{"content", testContent, config.Storage{}},
|
|
{"delivery order", testDeliveryOrder, config.Storage{}},
|
|
{"latest", testLatest, config.Storage{}},
|
|
{"naming", testNaming, config.Storage{}},
|
|
{"size", testSize, config.Storage{}},
|
|
{"seen", testSeen, config.Storage{}},
|
|
{"delete", testDelete, config.Storage{}},
|
|
{"purge", testPurge, config.Storage{}},
|
|
{"cap=10", testMsgCap, config.Storage{MailboxMsgCap: 10}},
|
|
{"cap=0", testNoMsgCap, config.Storage{MailboxMsgCap: 0}},
|
|
{"visit mailboxes", testVisitMailboxes, config.Storage{}},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
extHost := extension.NewHost()
|
|
store, destroy, err := factory(tc.conf, extHost)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer destroy()
|
|
|
|
s := suite{
|
|
T: t,
|
|
store: store,
|
|
extHost: extHost,
|
|
}
|
|
tc.test(s)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testMetadata verifies message metadata is stored and retrieved correctly.
|
|
func testMetadata(s suite) {
|
|
mailbox := "testmailbox"
|
|
from := &mail.Address{Name: "From Person", Address: "from@person.com"}
|
|
to := []*mail.Address{
|
|
{Name: "One Person", Address: "one@a.person.com"},
|
|
{Name: "Two Person", Address: "two@b.person.com"},
|
|
}
|
|
date := time.Now()
|
|
subject := "fantastic test subject line"
|
|
content := "doesn't matter"
|
|
delivery := &message.Delivery{
|
|
Meta: event.MessageMetadata{
|
|
// ID and Size will be determined by the Store.
|
|
Mailbox: mailbox,
|
|
From: from,
|
|
To: to,
|
|
Date: date,
|
|
Subject: subject,
|
|
Seen: false,
|
|
},
|
|
Reader: strings.NewReader(content),
|
|
}
|
|
id, err := s.store.AddMessage(delivery)
|
|
if err != nil {
|
|
s.Fatal(err)
|
|
}
|
|
if id == "" {
|
|
s.Fatal("Expected AddMessage() to return non-empty ID string")
|
|
}
|
|
// Retrieve and validate the message.
|
|
sm, err := s.store.GetMessage(mailbox, id)
|
|
if err != nil {
|
|
s.Fatal(err)
|
|
}
|
|
if sm.Mailbox() != mailbox {
|
|
s.Errorf("got mailbox %q, want: %q", sm.Mailbox(), mailbox)
|
|
}
|
|
if sm.ID() != id {
|
|
s.Errorf("got id %q, want: %q", sm.ID(), id)
|
|
}
|
|
if *sm.From() != *from {
|
|
s.Errorf("got from %v, want: %v", sm.From(), from)
|
|
}
|
|
if len(sm.To()) != len(to) {
|
|
s.Errorf("got len(to) = %v, want: %v", len(sm.To()), len(to))
|
|
} else {
|
|
for i, got := range sm.To() {
|
|
if *to[i] != *got {
|
|
s.Errorf("got to[%v] %v, want: %v", i, got, to[i])
|
|
}
|
|
}
|
|
}
|
|
if !sm.Date().Equal(date) {
|
|
s.Errorf("got date %v, want: %v", sm.Date(), date)
|
|
}
|
|
if sm.Subject() != subject {
|
|
s.Errorf("got subject %q, want: %q", sm.Subject(), subject)
|
|
}
|
|
if sm.Size() != int64(len(content)) {
|
|
s.Errorf("got size %v, want: %v", sm.Size(), len(content))
|
|
}
|
|
if sm.Seen() {
|
|
s.Errorf("got seen %v, want: false", sm.Seen())
|
|
}
|
|
}
|
|
|
|
// testContent generates some binary content and makes sure it is correctly retrieved.
|
|
func testContent(s suite) {
|
|
content := make([]byte, 5000)
|
|
for i := 0; i < len(content); i++ {
|
|
content[i] = byte(i % 256)
|
|
}
|
|
mailbox := "testmailbox"
|
|
from := &mail.Address{Name: "From Person", Address: "from@person.com"}
|
|
to := []*mail.Address{
|
|
{Name: "One Person", Address: "one@a.person.com"},
|
|
}
|
|
date := time.Now()
|
|
subject := "fantastic test subject line"
|
|
delivery := &message.Delivery{
|
|
Meta: event.MessageMetadata{
|
|
// ID and Size will be determined by the Store.
|
|
Mailbox: mailbox,
|
|
From: from,
|
|
To: to,
|
|
Date: date,
|
|
Subject: subject,
|
|
},
|
|
Reader: bytes.NewReader(content),
|
|
}
|
|
id, err := s.store.AddMessage(delivery)
|
|
require.NoError(s, err, "AddMessage() failed")
|
|
|
|
// Read stored message source.
|
|
m, err := s.store.GetMessage(mailbox, id)
|
|
require.NoError(s, err, "GetMessage() failed")
|
|
r, err := m.Source()
|
|
require.NoError(s, err, "Source() failed")
|
|
got, err := io.ReadAll(r)
|
|
require.NoError(s, err, "failed to read source")
|
|
err = r.Close()
|
|
require.NoError(s, err, "failed to close source reader")
|
|
|
|
// Verify source.
|
|
if len(got) != len(content) {
|
|
s.Errorf("Got len(content) == %v, want: %v", len(got), len(content))
|
|
}
|
|
errors := 0
|
|
for i, b := range got {
|
|
if b != content[i] {
|
|
s.Errorf("Got content[%v] == %v, want: %v", i, b, content[i])
|
|
errors++
|
|
}
|
|
if errors > 5 {
|
|
s.Fatalf("Too many content errors, aborting test.")
|
|
}
|
|
}
|
|
}
|
|
|
|
// testDeliveryOrder delivers several messages to the same mailbox, meanwhile querying its contents
|
|
// with a new GetMessages call each cycle.
|
|
func testDeliveryOrder(s suite) {
|
|
mailbox := "fred"
|
|
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
|
for i, subj := range subjects {
|
|
// Check mailbox count.
|
|
GetAndCountMessages(s.T, s.store, mailbox, i)
|
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
|
}
|
|
// Confirm delivery order.
|
|
msgs := GetAndCountMessages(s.T, s.store, mailbox, 5)
|
|
for i, want := range subjects {
|
|
got := msgs[i].Subject()
|
|
if got != want {
|
|
s.Errorf("Got subject %q, want %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// testLatest delivers several messages to the same mailbox, and confirms the id `latest` returns
|
|
// the last message sent.
|
|
func testLatest(s suite) {
|
|
mailbox := "fred"
|
|
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
|
for _, subj := range subjects {
|
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
|
}
|
|
// Confirm latest.
|
|
latest, err := s.store.GetMessage(mailbox, "latest")
|
|
if err != nil {
|
|
s.Fatal(err)
|
|
}
|
|
if latest == nil {
|
|
s.Fatalf("Got nil message, wanted most recent message for %v.", mailbox)
|
|
}
|
|
got := latest.Subject()
|
|
want := "echo"
|
|
if got != want {
|
|
s.Errorf("Got subject %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// testNaming ensures the store does not enforce local part mailbox naming.
|
|
func testNaming(s suite) {
|
|
DeliverToStore(s.T, s.store, "fred@fish.net", "disk #27", time.Now())
|
|
GetAndCountMessages(s.T, s.store, "fred", 0)
|
|
GetAndCountMessages(s.T, s.store, "fred@fish.net", 1)
|
|
}
|
|
|
|
// testSize verifies message content size metadata values.
|
|
func testSize(s suite) {
|
|
mailbox := "fred"
|
|
subjects := []string{"a", "br", "much longer than the others"}
|
|
sentIds := make([]string, len(subjects))
|
|
sentSizes := make([]int64, len(subjects))
|
|
for i, subj := range subjects {
|
|
id, size := DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
|
sentIds[i] = id
|
|
sentSizes[i] = size
|
|
}
|
|
for i, id := range sentIds {
|
|
msg, err := s.store.GetMessage(mailbox, id)
|
|
if err != nil {
|
|
s.Fatal(err)
|
|
}
|
|
want := sentSizes[i]
|
|
got := msg.Size()
|
|
if got != want {
|
|
s.Errorf("Got size %v, want: %v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// testSeen verifies a message can be marked as seen.
|
|
func testSeen(s suite) {
|
|
mailbox := "lisa"
|
|
id1, _ := DeliverToStore(s.T, s.store, mailbox, "whatever", time.Now())
|
|
id2, _ := DeliverToStore(s.T, s.store, mailbox, "hello?", time.Now())
|
|
// Confirm unseen.
|
|
msg, err := s.store.GetMessage(mailbox, id1)
|
|
if err != nil {
|
|
s.Fatal(err)
|
|
}
|
|
if msg.Seen() {
|
|
s.Errorf("got seen %v, want: false", msg.Seen())
|
|
}
|
|
// Mark id1 seen.
|
|
err = s.store.MarkSeen(mailbox, id1)
|
|
if err != nil {
|
|
s.Fatal(err)
|
|
}
|
|
// Verify id1 seen.
|
|
msg, err = s.store.GetMessage(mailbox, id1)
|
|
if err != nil {
|
|
s.Fatal(err)
|
|
}
|
|
if !msg.Seen() {
|
|
s.Errorf("id1 got seen %v, want: true", msg.Seen())
|
|
}
|
|
// Verify id2 still unseen.
|
|
msg, err = s.store.GetMessage(mailbox, id2)
|
|
if err != nil {
|
|
s.Fatal(err)
|
|
}
|
|
if msg.Seen() {
|
|
s.Errorf("id2 got seen %v, want: false", msg.Seen())
|
|
}
|
|
}
|
|
|
|
// testDelete creates and deletes some messages.
|
|
func testDelete(s suite) {
|
|
mailbox := "fred"
|
|
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
|
for _, subj := range subjects {
|
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
|
}
|
|
msgs := GetAndCountMessages(s.T, s.store, mailbox, len(subjects))
|
|
|
|
// Subscribe to events.
|
|
eventListener := s.extHost.Events.AfterMessageDeleted.AsyncTestListener("test", 2)
|
|
|
|
// Delete a couple messages.
|
|
deleteIDs := []string{msgs[1].ID(), msgs[3].ID()}
|
|
for _, id := range deleteIDs {
|
|
err := s.store.RemoveMessage(mailbox, id)
|
|
require.NoError(s, err)
|
|
}
|
|
|
|
// Confirm deletion.
|
|
subjects = []string{"alpha", "charlie", "echo"}
|
|
msgs = GetAndCountMessages(s.T, s.store, mailbox, len(subjects))
|
|
for i, want := range subjects {
|
|
got := msgs[i].Subject()
|
|
if got != want {
|
|
s.Errorf("Got subject %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
// Capture events and check correct IDs were emitted.
|
|
ev1, err := eventListener()
|
|
require.NoError(s, err)
|
|
ev2, err := eventListener()
|
|
require.NoError(s, err)
|
|
eventIDs := []string{ev1.ID, ev2.ID}
|
|
for _, id := range deleteIDs {
|
|
assert.Contains(s, eventIDs, id)
|
|
}
|
|
|
|
// Try appending one more.
|
|
DeliverToStore(s.T, s.store, mailbox, "foxtrot", time.Now())
|
|
subjects = []string{"alpha", "charlie", "echo", "foxtrot"}
|
|
msgs = GetAndCountMessages(s.T, s.store, mailbox, len(subjects))
|
|
for i, want := range subjects {
|
|
got := msgs[i].Subject()
|
|
if got != want {
|
|
s.Errorf("Got subject %q, want %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// testPurge makes sure mailboxes can be purged.
|
|
func testPurge(s suite) {
|
|
mailbox := "fred"
|
|
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
|
|
|
|
// Subscribe to events.
|
|
eventListener := s.extHost.Events.AfterMessageDeleted.AsyncTestListener("test", len(subjects))
|
|
|
|
// Populate mailbox.
|
|
for _, subj := range subjects {
|
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
|
}
|
|
GetAndCountMessages(s.T, s.store, mailbox, len(subjects))
|
|
|
|
// Purge and verify.
|
|
err := s.store.PurgeMessages(mailbox)
|
|
require.NoError(s, err)
|
|
GetAndCountMessages(s.T, s.store, mailbox, 0)
|
|
|
|
// Confirm events emitted.
|
|
gotEvents := []*event.MessageMetadata{}
|
|
for range subjects {
|
|
ev, err := eventListener()
|
|
if err != nil {
|
|
s.Error(err)
|
|
break
|
|
}
|
|
gotEvents = append(gotEvents, ev)
|
|
}
|
|
assert.Equal(s, len(subjects), len(gotEvents),
|
|
"expected delete event for each message in mailbox")
|
|
}
|
|
|
|
// testMsgCap verifies the message cap is enforced.
|
|
func testMsgCap(s suite) {
|
|
mbCap := 10
|
|
mailbox := "captain"
|
|
|
|
for i := 0; i < 20; i++ {
|
|
subj := fmt.Sprintf("subject %v", i)
|
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
|
msgs, err := s.store.GetMessages(mailbox)
|
|
if err != nil {
|
|
s.Fatalf("Failed to GetMessages for %q: %v", mailbox, err)
|
|
}
|
|
if len(msgs) > mbCap {
|
|
s.Errorf("Mailbox has %v messages, should be capped at %v", len(msgs), mbCap)
|
|
break
|
|
}
|
|
|
|
// Check that the first (oldest) message is correct.
|
|
first := i - mbCap + 1
|
|
if first < 0 {
|
|
first = 0
|
|
}
|
|
firstSubj := fmt.Sprintf("subject %v", first)
|
|
if firstSubj != msgs[0].Subject() {
|
|
s.Errorf("Got subject %q, wanted first subject: %q", msgs[0].Subject(), firstSubj)
|
|
}
|
|
}
|
|
}
|
|
|
|
// testNoMsgCap verfies a cap of 0 is not enforced.
|
|
func testNoMsgCap(s suite) {
|
|
mailbox := "captain"
|
|
for i := 0; i < 20; i++ {
|
|
subj := fmt.Sprintf("subject %v", i)
|
|
DeliverToStore(s.T, s.store, mailbox, subj, time.Now())
|
|
GetAndCountMessages(s.T, s.store, mailbox, i+1)
|
|
}
|
|
}
|
|
|
|
// testVisitMailboxes creates some mailboxes and confirms the VisitMailboxes method visits all of
|
|
// them.
|
|
func testVisitMailboxes(s suite) {
|
|
// Deliver 2 test messages to each of 5 mailboxes.
|
|
boxes := []string{"abby", "bill", "christa", "donald", "evelyn"}
|
|
for _, name := range boxes {
|
|
DeliverToStore(s.T, s.store, name, "Old Message", time.Now().Add(-24*time.Hour))
|
|
DeliverToStore(s.T, s.store, name, "New Message", time.Now())
|
|
}
|
|
|
|
// Verify message and mailbox counts.
|
|
nboxes := 0
|
|
err := s.store.VisitMailboxes(func(messages []storage.Message) bool {
|
|
nboxes++
|
|
name := "unknown"
|
|
if len(messages) > 0 {
|
|
name = messages[0].Mailbox()
|
|
}
|
|
|
|
assert.Len(s, messages, 2, "incorrect message count in mailbox %s", name)
|
|
return true
|
|
})
|
|
require.NoError(s, err, "VisitMailboxes() failed")
|
|
assert.Equal(s, 5, nboxes, "visited %v mailboxes, want: 5", nboxes)
|
|
}
|