From 9b3d3c2ea821586d669cfbd06f3b346f2531c2d2 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Fri, 16 Mar 2018 22:05:07 -0700 Subject: [PATCH] test: Finish initial storage test suite, closes #82 --- Makefile | 5 +- pkg/storage/file/fstore_test.go | 235 +---------------------------- pkg/test/storage_suite.go | 257 +++++++++++++++++++++++++++++++- 3 files changed, 260 insertions(+), 237 deletions(-) diff --git a/Makefile b/Makefile index 821c674..eacf4ba 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SHELL = /bin/sh SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*") PKGS := $(shell go list ./... | grep -v /vendor/) -.PHONY: all build clean fmt lint simplify test +.PHONY: all build clean fmt lint reflex simplify test commands = client inbucket @@ -34,3 +34,6 @@ lint: @test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'" @golint -set_exit_status $(PKGS) @go vet $(PKGS) + +reflex: + reflex -r '\.go$$' -- sh -c 'echo; date; echo; go test ./...' diff --git a/pkg/storage/file/fstore_test.go b/pkg/storage/file/fstore_test.go index 94380d9..18b1fe7 100644 --- a/pkg/storage/file/fstore_test.go +++ b/pkg/storage/file/fstore_test.go @@ -22,15 +22,13 @@ import ( // TestSuite runs storage package test suite on file store. func TestSuite(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) - defer teardownDataStore(ds) - test.StoreSuite(t, ds) - if t.Failed() { - // Wait for handler to finish logging. - time.Sleep(2 * time.Second) - // Dump buffered log data if there was a failure. - _, _ = io.Copy(os.Stderr, logbuf) - } + test.StoreSuite(t, func() (storage.Store, func(), error) { + ds, _ := setupDataStore(config.DataStoreConfig{}) + destroy := func() { + teardownDataStore(ds) + } + return ds, destroy, nil + }) } // Test directory structure created by filestore @@ -111,225 +109,6 @@ func TestFSDirStructure(t *testing.T) { } } -// TestFSVisitMailboxes tests VisitMailboxes -func TestFSVisitMailboxes(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) - defer teardownDataStore(ds) - boxes := []string{"abby", "bill", "christa", "donald", "evelyn"} - for _, name := range boxes { - // Create day old message - date := time.Now().Add(-24 * time.Hour) - deliverMessage(ds, name, "Old Message", date) - - // Create current message - date = time.Now() - deliverMessage(ds, name, "New Message", date) - } - - seen := 0 - err := ds.VisitMailboxes(func(messages []storage.StoreMessage) bool { - seen++ - count := len(messages) - if count != 2 { - t.Errorf("got: %v messages, want: 2", count) - } - return true - }) - assert.Nil(t, err) - assert.Equal(t, 5, seen) - - if t.Failed() { - // Wait for handler to finish logging - time.Sleep(2 * time.Second) - // Dump buffered log data if there was a failure - _, _ = io.Copy(os.Stderr, logbuf) - } -} - -// Test delivering several messages to the same mailbox, meanwhile querying its -// contents with a new mailbox object each time -func TestFSDeliverMany(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) - defer teardownDataStore(ds) - - mbName := "fred" - subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"} - - for i, subj := range subjects { - // Check number of messages - msgs, err := ds.GetMessages(mbName) - if err != nil { - t.Fatalf("Failed to GetMessages for %q: %v", mbName, err) - } - assert.Equal(t, i, len(msgs), "Expected %v message(s), but got %v", i, len(msgs)) - - // Add a message - deliverMessage(ds, mbName, subj, time.Now()) - } - - msgs, err := ds.GetMessages(mbName) - if err != nil { - t.Fatalf("Failed to GetMessages for %q: %v", mbName, err) - } - assert.Equal(t, len(subjects), len(msgs), "Expected %v message(s), but got %v", - len(subjects), len(msgs)) - - // Confirm delivery order - for i, expect := range subjects { - subj := msgs[i].Subject() - assert.Equal(t, expect, subj, "Expected subject %q, got %q", expect, subj) - } - - if t.Failed() { - // Wait for handler to finish logging - time.Sleep(2 * time.Second) - // Dump buffered log data if there was a failure - _, _ = io.Copy(os.Stderr, logbuf) - } -} - -// Test deleting messages -func TestFSDelete(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) - defer teardownDataStore(ds) - - mbName := "fred" - subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"} - - for _, subj := range subjects { - // Add a message - deliverMessage(ds, mbName, subj, time.Now()) - } - - msgs, err := ds.GetMessages(mbName) - if err != nil { - t.Fatalf("Failed to GetMessages for %q: %v", mbName, err) - } - assert.Equal(t, len(subjects), len(msgs), "Expected %v message(s), but got %v", - len(subjects), len(msgs)) - - // Delete a couple messages - err = ds.RemoveMessage(mbName, msgs[1].ID()) - if err != nil { - t.Fatal(err) - } - err = ds.RemoveMessage(mbName, msgs[3].ID()) - if err != nil { - t.Fatal(err) - } - - // Confirm deletion - msgs, err = ds.GetMessages(mbName) - if err != nil { - t.Fatalf("Failed to GetMessages for %q: %v", mbName, err) - } - - subjects = []string{"alpha", "charlie", "echo"} - assert.Equal(t, len(subjects), len(msgs), "Expected %v message(s), but got %v", - len(subjects), len(msgs)) - for i, expect := range subjects { - subj := msgs[i].Subject() - assert.Equal(t, expect, subj, "Expected subject %q, got %q", expect, subj) - } - - // Try appending one more - deliverMessage(ds, mbName, "foxtrot", time.Now()) - - msgs, err = ds.GetMessages(mbName) - if err != nil { - t.Fatalf("Failed to GetMessages for %q: %v", mbName, err) - } - - subjects = []string{"alpha", "charlie", "echo", "foxtrot"} - assert.Equal(t, len(subjects), len(msgs), "Expected %v message(s), but got %v", - len(subjects), len(msgs)) - for i, expect := range subjects { - subj := msgs[i].Subject() - assert.Equal(t, expect, subj, "Expected subject %q, got %q", expect, subj) - } - - if t.Failed() { - // Wait for handler to finish logging - time.Sleep(2 * time.Second) - // Dump buffered log data if there was a failure - _, _ = io.Copy(os.Stderr, logbuf) - } -} - -// Test purging a mailbox -func TestFSPurge(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) - defer teardownDataStore(ds) - - mbName := "fred" - subjects := []string{"alpha", "bravo", "charlie", "delta", "echo"} - - for _, subj := range subjects { - // Add a message - deliverMessage(ds, mbName, subj, time.Now()) - } - - msgs, err := ds.GetMessages(mbName) - if err != nil { - t.Fatalf("Failed to GetMessages for %q: %v", mbName, err) - } - assert.Equal(t, len(subjects), len(msgs), "Expected %v message(s), but got %v", - len(subjects), len(msgs)) - - // Purge mailbox - err = ds.PurgeMessages(mbName) - assert.Nil(t, err) - - // Confirm deletion - msgs, err = ds.GetMessages(mbName) - if err != nil { - t.Fatalf("Failed to GetMessages for %q: %v", mbName, err) - } - - assert.Equal(t, len(msgs), 0, "Expected mailbox to have zero messages, got %v", len(msgs)) - - if t.Failed() { - // Wait for handler to finish logging - time.Sleep(2 * time.Second) - // Dump buffered log data if there was a failure - _, _ = io.Copy(os.Stderr, logbuf) - } -} - -// Test message size calculation -func TestFSSize(t *testing.T) { - ds, logbuf := setupDataStore(config.DataStoreConfig{}) - defer teardownDataStore(ds) - - mbName := "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 { - // Add a message - id, size := deliverMessage(ds, mbName, subj, time.Now()) - sentIds[i] = id - sentSizes[i] = size - } - - for i, id := range sentIds { - msg, err := ds.GetMessage(mbName, id) - assert.Nil(t, err) - - expect := sentSizes[i] - size := msg.Size() - assert.Equal(t, expect, size, "Expected size of %v, got %v", expect, size) - } - - if t.Failed() { - // Wait for handler to finish logging - time.Sleep(2 * time.Second) - // Dump buffered log data if there was a failure - _, _ = io.Copy(os.Stderr, logbuf) - } -} - // Test missing files func TestFSMissing(t *testing.T) { ds, logbuf := setupDataStore(config.DataStoreConfig{}) diff --git a/pkg/test/storage_suite.go b/pkg/test/storage_suite.go index fd71774..80e8626 100644 --- a/pkg/test/storage_suite.go +++ b/pkg/test/storage_suite.go @@ -1,6 +1,9 @@ package test import ( + "bytes" + "fmt" + "io/ioutil" "net/mail" "strings" "testing" @@ -10,23 +13,37 @@ import ( "github.com/jhillyerd/inbucket/pkg/storage" ) -// StoreSuite runs a set of general tests on the provided Store -func StoreSuite(t *testing.T, store storage.Store) { +// StoreFactory returns a new store for the test suite. +type StoreFactory func() (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) }{ {"metadata", testMetadata}, + {"content", testContent}, + {"delivery order", testDeliveryOrder}, + {"size", testSize}, + {"delete", testDelete}, + {"purge", testPurge}, + {"visit mailboxes", testVisitMailboxes}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + store, destroy, err := factory() + if err != nil { + t.Fatal(err) + } tc.test(t, store) + destroy() }) } } -func testMetadata(t *testing.T, ds storage.Store) { - // Store a message +// 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{ @@ -38,7 +55,7 @@ func testMetadata(t *testing.T, ds storage.Store) { content := "doesn't matter" delivery := &message.Delivery{ Meta: message.Metadata{ - // ID and Size will be determined by the Store + // ID and Size will be determined by the Store. Mailbox: mailbox, From: from, To: to, @@ -47,15 +64,15 @@ func testMetadata(t *testing.T, ds storage.Store) { }, Reader: strings.NewReader(content), } - id, err := ds.AddMessage(delivery) + 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 := ds.GetMessage(mailbox, id) + // Retrieve and validate the message. + sm, err := store.GetMessage(mailbox, id) if err != nil { t.Fatal(err) } @@ -87,3 +104,227 @@ func testMetadata(t *testing.T, ds storage.Store) { t.Errorf("got size %v, want: %v", sm.Size(), len(content)) } } + +// 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.RawReader() + 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) + deliverMessage(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) + } + } +} + +// 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 := deliverMessage(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) + } + } +} + +// 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 { + deliverMessage(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. + deliverMessage(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 { + deliverMessage(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) +} + +// 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 { + deliverMessage(t, ds, name, "Old Message", time.Now().Add(-24*time.Hour)) + deliverMessage(t, ds, name, "New Message", time.Now()) + } + seen := 0 + err := ds.VisitMailboxes(func(messages []storage.StoreMessage) 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) + } +} + +// deliverMessage creates and delivers a message to the specific mailbox, returning the size of the +// generated message. +func deliverMessage( + 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.StoreMessage { + 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, want: %v", len(msgs), count) + } + return msgs +}