1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-17 17:47:03 +00:00
Files
go-inbucket/pkg/test/storage_suite.go
James Hillyerd 12f98868ba smtp/policy: Store messages with full naming for #33
- Added some tests to make sure stores do not enforce their own naming.
- Improve recipient structured logging.
2018-04-07 13:06:56 -07:00

421 lines
12 KiB
Go

package test
import (
"bytes"
"fmt"
"io/ioutil"
"net/mail"
"strings"
"testing"
"time"
"github.com/jhillyerd/inbucket/pkg/config"
"github.com/jhillyerd/inbucket/pkg/message"
"github.com/jhillyerd/inbucket/pkg/storage"
)
// StoreFactory returns a new store for the test suite.
type StoreFactory func(config.Storage) (store storage.Store, destroy func(), err error)
// StoreSuite runs a set of general tests on the provided Store.
func StoreSuite(t *testing.T, factory StoreFactory) {
testCases := []struct {
name string
test func(*testing.T, storage.Store)
conf config.Storage
}{
{"metadata", testMetadata, config.Storage{}},
{"content", testContent, config.Storage{}},
{"delivery order", testDeliveryOrder, 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) {
store, destroy, err := factory(tc.conf)
if err != nil {
t.Fatal(err)
}
tc.test(t, store)
destroy()
})
}
}
// testMetadata verifies message metadata is stored and retrieved correctly.
func testMetadata(t *testing.T, store storage.Store) {
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: message.Metadata{
// 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 := store.AddMessage(delivery)
if err != nil {
t.Fatal(err)
}
if id == "" {
t.Fatal("Expected AddMessage() to return non-empty ID string")
}
// Retrieve and validate the message.
sm, err := store.GetMessage(mailbox, id)
if err != nil {
t.Fatal(err)
}
if sm.Mailbox() != mailbox {
t.Errorf("got mailbox %q, want: %q", sm.Mailbox(), mailbox)
}
if sm.ID() != id {
t.Errorf("got id %q, want: %q", sm.ID(), id)
}
if *sm.From() != *from {
t.Errorf("got from %v, want: %v", sm.From(), from)
}
if len(sm.To()) != len(to) {
t.Errorf("got len(to) = %v, want: %v", len(sm.To()), len(to))
} else {
for i, got := range sm.To() {
if *to[i] != *got {
t.Errorf("got to[%v] %v, want: %v", i, got, to[i])
}
}
}
if !sm.Date().Equal(date) {
t.Errorf("got date %v, want: %v", sm.Date(), date)
}
if sm.Subject() != subject {
t.Errorf("got subject %q, want: %q", sm.Subject(), subject)
}
if sm.Size() != int64(len(content)) {
t.Errorf("got size %v, want: %v", sm.Size(), len(content))
}
if sm.Seen() {
t.Errorf("got seen %v, want: false", sm.Seen())
}
}
// testContent generates some binary content and makes sure it is correctly retrieved.
func testContent(t *testing.T, store storage.Store) {
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: message.Metadata{
// 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 := store.AddMessage(delivery)
if err != nil {
t.Fatal(err)
}
// Get and check.
m, err := store.GetMessage(mailbox, id)
if err != nil {
t.Fatal(err)
}
r, err := m.Source()
if err != nil {
t.Fatal(err)
}
got, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal(err)
}
if len(got) != len(content) {
t.Errorf("Got len(content) == %v, want: %v", len(got), len(content))
}
errors := 0
for i, b := range got {
if b != content[i] {
t.Errorf("Got content[%v] == %v, want: %v", i, b, content[i])
errors++
}
if errors > 5 {
t.Fatalf("Too many content errors, aborting test.")
break
}
}
}
// testDeliveryOrder delivers several messages to the same mailbox, meanwhile querying its contents
// with a new GetMessages call each cycle.
func testDeliveryOrder(t *testing.T, store storage.Store) {
mailbox := "fred"
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
for i, subj := range subjects {
// Check mailbox count.
GetAndCountMessages(t, store, mailbox, i)
DeliverToStore(t, store, mailbox, subj, time.Now())
}
// Confirm delivery order.
msgs := GetAndCountMessages(t, store, mailbox, 5)
for i, want := range subjects {
got := msgs[i].Subject()
if got != want {
t.Errorf("Got subject %q, want %q", got, want)
}
}
}
// testNaming ensures the store does not enforce local part mailbox naming.
func testNaming(t *testing.T, store storage.Store) {
DeliverToStore(t, store, "fred@fish.net", "disk #27", time.Now())
GetAndCountMessages(t, store, "fred", 0)
GetAndCountMessages(t, store, "fred@fish.net", 1)
}
// testSize verifies message contnet size metadata values.
func testSize(t *testing.T, store storage.Store) {
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(t, store, mailbox, subj, time.Now())
sentIds[i] = id
sentSizes[i] = size
}
for i, id := range sentIds {
msg, err := store.GetMessage(mailbox, id)
if err != nil {
t.Fatal(err)
}
want := sentSizes[i]
got := msg.Size()
if got != want {
t.Errorf("Got size %v, want: %v", got, want)
}
}
}
// testSeen verifies a message can be marked as seen.
func testSeen(t *testing.T, store storage.Store) {
mailbox := "lisa"
id1, _ := DeliverToStore(t, store, mailbox, "whatever", time.Now())
id2, _ := DeliverToStore(t, store, mailbox, "hello?", time.Now())
// Confirm unseen.
msg, err := store.GetMessage(mailbox, id1)
if err != nil {
t.Fatal(err)
}
if msg.Seen() {
t.Errorf("got seen %v, want: false", msg.Seen())
}
// Mark id1 seen.
err = store.MarkSeen(mailbox, id1)
if err != nil {
t.Fatal(err)
}
// Verify id1 seen.
msg, err = store.GetMessage(mailbox, id1)
if err != nil {
t.Fatal(err)
}
if !msg.Seen() {
t.Errorf("id1 got seen %v, want: true", msg.Seen())
}
// Verify id2 still unseen.
msg, err = store.GetMessage(mailbox, id2)
if err != nil {
t.Fatal(err)
}
if msg.Seen() {
t.Errorf("id2 got seen %v, want: false", msg.Seen())
}
}
// testDelete creates and deletes some messages.
func testDelete(t *testing.T, store storage.Store) {
mailbox := "fred"
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
for _, subj := range subjects {
DeliverToStore(t, store, mailbox, subj, time.Now())
}
msgs := GetAndCountMessages(t, store, mailbox, len(subjects))
// Delete a couple messages.
err := store.RemoveMessage(mailbox, msgs[1].ID())
if err != nil {
t.Fatal(err)
}
err = store.RemoveMessage(mailbox, msgs[3].ID())
if err != nil {
t.Fatal(err)
}
// Confirm deletion.
subjects = []string{"alpha", "charlie", "echo"}
msgs = GetAndCountMessages(t, store, mailbox, len(subjects))
for i, want := range subjects {
got := msgs[i].Subject()
if got != want {
t.Errorf("Got subject %q, want %q", got, want)
}
}
// Try appending one more.
DeliverToStore(t, store, mailbox, "foxtrot", time.Now())
subjects = []string{"alpha", "charlie", "echo", "foxtrot"}
msgs = GetAndCountMessages(t, store, mailbox, len(subjects))
for i, want := range subjects {
got := msgs[i].Subject()
if got != want {
t.Errorf("Got subject %q, want %q", got, want)
}
}
}
// testPurge makes sure mailboxes can be purged.
func testPurge(t *testing.T, store storage.Store) {
mailbox := "fred"
subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"}
for _, subj := range subjects {
DeliverToStore(t, store, mailbox, subj, time.Now())
}
GetAndCountMessages(t, store, mailbox, len(subjects))
// Purge and verify.
err := store.PurgeMessages(mailbox)
if err != nil {
t.Fatal(err)
}
GetAndCountMessages(t, store, mailbox, 0)
}
// testMsgCap verifies the message cap is enforced.
func testMsgCap(t *testing.T, store storage.Store) {
mbCap := 10
mailbox := "captain"
for i := 0; i < 20; i++ {
subj := fmt.Sprintf("subject %v", i)
DeliverToStore(t, store, mailbox, subj, time.Now())
msgs, err := store.GetMessages(mailbox)
if err != nil {
t.Fatalf("Failed to GetMessages for %q: %v", mailbox, err)
}
if len(msgs) > mbCap {
t.Errorf("Mailbox has %v messages, should be capped at %v", len(msgs), mbCap)
break
}
// Check that the first message is correct.
first := i - mbCap + 1
if first < 0 {
first = 0
}
firstSubj := fmt.Sprintf("subject %v", first)
if firstSubj != msgs[0].Subject() {
t.Errorf("Got subject %q, wanted first subject: %q", msgs[0].Subject(), firstSubj)
}
}
}
// testNoMsgCap verfies a cap of 0 is not enforced.
func testNoMsgCap(t *testing.T, store storage.Store) {
mailbox := "captain"
for i := 0; i < 20; i++ {
subj := fmt.Sprintf("subject %v", i)
DeliverToStore(t, store, mailbox, subj, time.Now())
GetAndCountMessages(t, store, mailbox, i+1)
}
}
// testVisitMailboxes creates some mailboxes and confirms the VisitMailboxes method visits all of
// them.
func testVisitMailboxes(t *testing.T, ds storage.Store) {
boxes := []string{"abby", "bill", "christa", "donald", "evelyn"}
for _, name := range boxes {
DeliverToStore(t, ds, name, "Old Message", time.Now().Add(-24*time.Hour))
DeliverToStore(t, ds, name, "New Message", time.Now())
}
seen := 0
err := ds.VisitMailboxes(func(messages []storage.Message) bool {
seen++
count := len(messages)
if count != 2 {
t.Errorf("got: %v messages, want: 2", count)
}
return true
})
if err != nil {
t.Error(err)
}
if seen != 5 {
t.Errorf("saw %v messages in total, want: 5", seen)
}
}
// DeliverToStore creates and delivers a message to the specific mailbox, returning the size of the
// generated message.
func DeliverToStore(
t *testing.T,
store storage.Store,
mailbox string,
subject string,
date time.Time,
) (string, int64) {
t.Helper()
meta := message.Metadata{
Mailbox: mailbox,
To: []*mail.Address{{Name: "Some Body", Address: "somebody@host"}},
From: &mail.Address{Name: "Some B. Else", Address: "somebodyelse@host"},
Subject: subject,
Date: date,
}
testMsg := fmt.Sprintf("To: %s\r\nFrom: %s\r\nSubject: %s\r\n\r\nTest Body\r\n",
meta.To[0].Address, meta.From.Address, subject)
delivery := &message.Delivery{
Meta: meta,
Reader: ioutil.NopCloser(strings.NewReader(testMsg)),
}
id, err := store.AddMessage(delivery)
if err != nil {
t.Fatal(err)
}
return id, int64(len(testMsg))
}
// GetAndCountMessages is a test helper that expects to receive count messages or fails the test, it
// also checks return error.
func GetAndCountMessages(t *testing.T, s storage.Store, mailbox string, count int) []storage.Message {
t.Helper()
msgs, err := s.GetMessages(mailbox)
if err != nil {
t.Fatalf("Failed to GetMessages for %q: %v", mailbox, err)
}
if len(msgs) != count {
t.Errorf("Got %v messages for %q, want: %v", len(msgs), mailbox, count)
}
return msgs
}