From d8c6364622a8065e5b03c0a33cb9ddae2cc117e4 Mon Sep 17 00:00:00 2001 From: Ralph Slooten Date: Fri, 7 Oct 2022 19:48:50 +1300 Subject: [PATCH] Testing: Add API tests --- .github/workflows/tests.yml | 2 +- server/server_test.go | 305 ++++++++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 server/server_test.go diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 237ff5a..82ca334 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - - run: go test ./storage -v + - run: go test ./storage ./server -v - run: go test ./storage -bench=. # build the assets diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 0000000..3da915e --- /dev/null +++ b/server/server_test.go @@ -0,0 +1,305 @@ +package server + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/server/apiv1" + "github.com/axllent/mailpit/storage" + "github.com/jhillyerd/enmime" +) + +var ( + putDataStruct struct { + Read bool `json:"read"` + IDs []string `json:"ids"` + } +) + +func Test_APIv1(t *testing.T) { + setup() + defer storage.Close() + + r := defaultRoutes() + + ts := httptest.NewServer(r) + defer ts.Close() + + m, err := fetchMessages(ts.URL + "/api/v1/messages") + if err != nil { + t.Errorf(err.Error()) + } + + // check count of empty database + assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0) + + // insert 100 + t.Log("Insert 100 messages") + insertEmailData(t) + assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100) + + // store this for later tests + + m, err = fetchMessages(ts.URL + "/api/v1/messages") + if err != nil { + t.Errorf(err.Error()) + } + + // read first 10 + t.Log("Read first 10 messages") + putIDS := []string{} + for indx, msg := range m.Messages { + if indx == 10 { + break + } + + _, err := clientGet(ts.URL + "/api/v1/message/" + msg.ID) + if err != nil { + t.Errorf(err.Error()) + } + + // store for later + putIDS = append(putIDS, msg.ID) + } + assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100) + + // mark first 10 as unread + t.Log("Mark first 10 as unread") + putData := putDataStruct + putData.IDs = putIDS + j, err := json.Marshal(putData) + if err != nil { + t.Errorf(err.Error()) + } + _, err = clientPut(ts.URL+"/api/v1/messages", string(j)) + if err != nil { + t.Errorf(err.Error()) + } + assertStatsEqual(t, ts.URL+"/api/v1/messages", 100, 100) + + // mark first 10 as read + t.Log("Mark first 10 as read") + putData.Read = true + j, err = json.Marshal(putData) + if err != nil { + t.Errorf(err.Error()) + } + _, err = clientPut(ts.URL+"/api/v1/messages", string(j)) + if err != nil { + t.Errorf(err.Error()) + } + assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 100) + + // search + assertSearchEqual(t, ts.URL+"/api/v1/search", "from-1@example.com", 1) + assertSearchEqual(t, ts.URL+"/api/v1/search", "to:from-1@example.com", 0) + assertSearchEqual(t, ts.URL+"/api/v1/search", "from:@example.com", 100) + assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line\"", 100) + assertSearchEqual(t, ts.URL+"/api/v1/search", "subject:\"Subject line 17 end\"", 1) + assertSearchEqual(t, ts.URL+"/api/v1/search", "!thisdoesnotexist", 100) + assertSearchEqual(t, ts.URL+"/api/v1/search", "-thisdoesnotexist", 100) + assertSearchEqual(t, ts.URL+"/api/v1/search", "thisdoesnotexist", 0) + + // delete first 10 + t.Log("Delete first 10") + _, err = clientDelete(ts.URL+"/api/v1/messages", string(j)) + if err != nil { + t.Errorf(err.Error()) + } + assertStatsEqual(t, ts.URL+"/api/v1/messages", 90, 90) + + // mark all as read + putData.Read = true + putData.IDs = []string{} + j, err = json.Marshal(putData) + if err != nil { + t.Errorf(err.Error()) + } + + t.Log("Mark all read") + _, err = clientPut(ts.URL+"/api/v1/messages", string(j)) + if err != nil { + t.Errorf(err.Error()) + } + assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 90) + + // delete all + t.Log("Delete all messages") + _, err = clientDelete(ts.URL+"/api/v1/messages", "{}") + if err != nil { + t.Errorf("Expected nil, received %s", err.Error()) + } + assertStatsEqual(t, ts.URL+"/api/v1/messages", 0, 0) +} + +func setup() { + config.NoLogging = true + config.MaxMessages = 0 + config.DataFile = "" + + if err := storage.InitDB(); err != nil { + panic(err) + } +} + +func assertStatsEqual(t *testing.T, uri string, unread, total int) { + m := apiv1.MessagesResult{} + + data, err := clientGet(uri) + if err != nil { + t.Errorf(err.Error()) + return + } + + if err := json.Unmarshal(data, &m); err != nil { + t.Errorf(err.Error()) + return + } + + assertEqual(t, unread, m.Unread, "wrong unread count") + assertEqual(t, total, m.Total, "wrong total count") +} + +func assertSearchEqual(t *testing.T, uri, query string, count int) { + t.Logf("Test search: %s", query) + m := apiv1.MessagesResult{} + + data, err := clientGet(uri + "?query=" + url.QueryEscape(query)) + if err != nil { + t.Errorf(err.Error()) + return + } + + if err := json.Unmarshal(data, &m); err != nil { + t.Errorf(err.Error()) + return + } + + assertEqual(t, count, m.Count, "wrong search results count") +} + +func insertEmailData(t *testing.T) { + for i := 0; i < 100; i++ { + msg := enmime.Builder(). + From(fmt.Sprintf("From %d", i), fmt.Sprintf("from-%d@example.com", i)). + Subject(fmt.Sprintf("Subject line %d end", i)). + Text([]byte(fmt.Sprintf("This is the email body %d .", i))). + To(fmt.Sprintf("To %d", i), fmt.Sprintf("to-%d@example.com", i)) + + env, err := msg.Build() + if err != nil { + t.Log("error ", err) + t.Fail() + } + + buf := new(bytes.Buffer) + + if err := env.Encode(buf); err != nil { + t.Log("error ", err) + t.Fail() + } + + if _, err := storage.Store(buf.Bytes()); err != nil { + t.Log("error ", err) + t.Fail() + } + } + +} + +func fetchMessages(url string) (apiv1.MessagesResult, error) { + m := apiv1.MessagesResult{} + + data, err := clientGet(url) + if err != nil { + return m, err + } + + if err := json.Unmarshal(data, &m); err != nil { + return m, err + } + + return m, nil +} + +func clientGet(url string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode) + } + + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + + return data, err +} + +func clientDelete(url, body string) ([]byte, error) { + client := new(http.Client) + + b := strings.NewReader(body) + req, err := http.NewRequest("DELETE", url, b) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode) + } + + data, err := ioutil.ReadAll(resp.Body) + + return data, err +} + +func clientPut(url, body string) ([]byte, error) { + client := new(http.Client) + + b := strings.NewReader(body) + req, err := http.NewRequest("PUT", url, b) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s returned status %d", url, resp.StatusCode) + } + + data, err := ioutil.ReadAll(resp.Body) + + return data, err +} + +func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { + if a == b { + return + } + message = fmt.Sprintf("%s: \"%v\" != \"%v\"", message, a, b) + t.Fatal(message) +}