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 }