From b9535c126cbc848db9f2378dca87ab879d0b97f0 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Thu, 25 Feb 2016 14:42:10 -0800 Subject: [PATCH] Implement REST API v1 - Add rest package with REST-only controller, tests - Remove individual shell scripts in favor of rest-apivh1.sh --- etc/mailbox-list.sh | 2 - etc/mailbox-purge.sh | 2 - etc/message-body.sh | 2 - etc/message-delete.sh | 2 - etc/message-source.sh | 2 - etc/rest-apiv1.sh | 108 +++++++ inbucket.go | 2 + rest/apiv1_controller.go | 207 ++++++++++++++ rest/apiv1_controller_test.go | 524 ++++++++++++++++++++++++++++++++++ rest/routes.go | 14 + 10 files changed, 855 insertions(+), 10 deletions(-) delete mode 100755 etc/mailbox-list.sh delete mode 100755 etc/mailbox-purge.sh delete mode 100755 etc/message-body.sh delete mode 100755 etc/message-delete.sh delete mode 100755 etc/message-source.sh create mode 100755 etc/rest-apiv1.sh create mode 100644 rest/apiv1_controller.go create mode 100644 rest/apiv1_controller_test.go create mode 100644 rest/routes.go diff --git a/etc/mailbox-list.sh b/etc/mailbox-list.sh deleted file mode 100755 index b0f20bb..0000000 --- a/etc/mailbox-list.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -curl -i -H "Accept: application/json" --noproxy localhost "http://localhost:9000/mailbox/$1" diff --git a/etc/mailbox-purge.sh b/etc/mailbox-purge.sh deleted file mode 100755 index 4d15336..0000000 --- a/etc/mailbox-purge.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -curl -i -H "Accept: application/json" --noproxy localhost -X DELETE http://localhost:9000/mailbox/$1 diff --git a/etc/message-body.sh b/etc/message-body.sh deleted file mode 100755 index 3e593ed..0000000 --- a/etc/message-body.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1/$2 diff --git a/etc/message-delete.sh b/etc/message-delete.sh deleted file mode 100755 index c2fae1a..0000000 --- a/etc/message-delete.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -curl -i -H "Accept: application/json" --noproxy localhost -X DELETE http://localhost:9000/mailbox/$1/$2 diff --git a/etc/message-source.sh b/etc/message-source.sh deleted file mode 100755 index c4f52ec..0000000 --- a/etc/message-source.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1/$2/source diff --git a/etc/rest-apiv1.sh b/etc/rest-apiv1.sh new file mode 100755 index 0000000..39ad8a7 --- /dev/null +++ b/etc/rest-apiv1.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# rest-apiv1.sh +# description: Script to access Inbucket REST API version 1 + +API_HOST="localhost" +URL_ROOT="http://$API_HOST:9000/api/v1" + +set -eo pipefail +[ $TRACE ] && set -x + +usage() { + echo "Usage: $0 [argument1 [argument2 [..]]]" >&2 + echo >&2 + echo "Options:" >&2 + echo " -h - show this help" >&2 + echo " -i - show HTTP headers" >&2 + echo >&2 + echo "Commands:" >&2 + echo " list - list mailbox contents" >&2 + echo " body - print message body" >&2 + echo " source - print message source" >&2 + echo " delete - delete message" >&2 + echo " purge - delete all messages in mailbox" >&2 +} + +arg_check() { + declare command="$1" expected="$2" received="$3" + if [ $expected != $received ]; then + echo "Error: Command '$command' requires $expected arguments, but received $received" >&2 + echo >&2 + usage + exit 1 + fi +} + +main() { + # Process options + local curl_opts="" + for arg in $*; do + if [[ $arg == -* ]]; then + case "$arg" in + -h) + usage + exit + ;; + -i) + curl_opts="$curl_opts -i" + ;; + **) + echo "Unknown option: $arg" >&2 + echo + usage + exit 1 + ;; + esac + shift + else + break + fi + done + + # Store command + declare command="$1" + shift + + local url="" + local method="GET" + + case "$command" in + body) + arg_check "$command" 2 $# + url="$URL_ROOT/mailbox/$1/$2" + ;; + delete) + arg_check "$command" 2 $# + method=DELETE + url="$URL_ROOT/mailbox/$1/$2" + ;; + list) + arg_check "$command" 1 $# + url="$URL_ROOT/mailbox/$1" + ;; + purge) + arg_check "$command" 1 $# + method=DELETE + url="$URL_ROOT/mailbox/$1" + ;; + source) + arg_check "$command" 2 $# + url="$URL_ROOT/mailbox/$1/$2/source" + ;; + *) + echo "Unknown command $command" >&2 + echo >&2 + usage + exit 1 + ;; + esac + + curl $curl_opts -H "Accept: application/json" --noproxy "$API_HOST" -X "$method" "$url" +} + +if [ $# -lt 1 ]; then + usage + exit 1 +fi + +main $* diff --git a/inbucket.go b/inbucket.go index abb24ea..875f659 100644 --- a/inbucket.go +++ b/inbucket.go @@ -15,6 +15,7 @@ import ( "github.com/jhillyerd/inbucket/httpd" "github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/pop3d" + "github.com/jhillyerd/inbucket/rest" "github.com/jhillyerd/inbucket/smtpd" "github.com/jhillyerd/inbucket/webui" ) @@ -125,6 +126,7 @@ func main() { // Start HTTP server httpd.Initialize(config.GetWebConfig(), ds) webui.SetupRoutes(httpd.Router) + rest.SetupRoutes(httpd.Router) go httpd.Start() // Start POP3 server diff --git a/rest/apiv1_controller.go b/rest/apiv1_controller.go new file mode 100644 index 0000000..2dd0800 --- /dev/null +++ b/rest/apiv1_controller.go @@ -0,0 +1,207 @@ +package rest + +import ( + "fmt" + "io" + "net/http" + "net/mail" + "time" + + "github.com/jhillyerd/inbucket/httpd" + "github.com/jhillyerd/inbucket/log" + "github.com/jhillyerd/inbucket/smtpd" +) + +// JSONMessageHeaderV1 contains the basic header data for a message +type JSONMessageHeaderV1 struct { + Mailbox string + ID string `json:"Id"` + From string + Subject string + Date time.Time + Size int64 +} + +// JSONMessageV1 contains the same data as the header plus a JSONMessageBody +type JSONMessageV1 struct { + Mailbox string + ID string `json:"Id"` + From string + Subject string + Date time.Time + Size int64 + Body *JSONMessageBodyV1 + Header mail.Header +} + +// JSONMessageBodyV1 contains the Text and HTML versions of the message body +type JSONMessageBodyV1 struct { + Text string + HTML string `json:"Html"` +} + +// MailboxListV1 renders a list of messages in a mailbox +func MailboxListV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { + // Don't have to validate these aren't empty, Gorilla returns 404 + name, err := smtpd.ParseMailboxName(ctx.Vars["name"]) + if err != nil { + return err + } + mb, err := ctx.DataStore.MailboxFor(name) + if err != nil { + // This doesn't indicate not found, likely an IO error + return fmt.Errorf("Failed to get mailbox for %q: %v", name, err) + } + messages, err := mb.GetMessages() + if err != nil { + // This doesn't indicate empty, likely an IO error + return fmt.Errorf("Failed to get messages for %v: %v", name, err) + } + log.Tracef("Got %v messsages", len(messages)) + + jmessages := make([]*JSONMessageHeaderV1, len(messages)) + for i, msg := range messages { + jmessages[i] = &JSONMessageHeaderV1{ + Mailbox: name, + ID: msg.ID(), + From: msg.From(), + Subject: msg.Subject(), + Date: msg.Date(), + Size: msg.Size(), + } + } + return httpd.RenderJSON(w, jmessages) +} + +// MailboxShowV1 renders a particular message from a mailbox +func MailboxShowV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { + // Don't have to validate these aren't empty, Gorilla returns 404 + id := ctx.Vars["id"] + name, err := smtpd.ParseMailboxName(ctx.Vars["name"]) + if err != nil { + return err + } + mb, err := ctx.DataStore.MailboxFor(name) + if err != nil { + // This doesn't indicate not found, likely an IO error + return fmt.Errorf("Failed to get mailbox for %q: %v", name, err) + } + msg, err := mb.GetMessage(id) + if err == smtpd.ErrNotExist { + http.NotFound(w, req) + return nil + } + if err != nil { + // This doesn't indicate empty, likely an IO error + return fmt.Errorf("GetMessage(%q) failed: %v", id, err) + } + header, err := msg.ReadHeader() + if err != nil { + return fmt.Errorf("ReadHeader(%q) failed: %v", id, err) + } + mime, err := msg.ReadBody() + if err != nil { + return fmt.Errorf("ReadBody(%q) failed: %v", id, err) + } + + return httpd.RenderJSON(w, + &JSONMessageV1{ + Mailbox: name, + ID: msg.ID(), + From: msg.From(), + Subject: msg.Subject(), + Date: msg.Date(), + Size: msg.Size(), + Header: header.Header, + Body: &JSONMessageBodyV1{ + Text: mime.Text, + HTML: mime.Html, + }, + }) +} + +// MailboxPurgeV1 deletes all messages from a mailbox +func MailboxPurgeV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { + // Don't have to validate these aren't empty, Gorilla returns 404 + name, err := smtpd.ParseMailboxName(ctx.Vars["name"]) + if err != nil { + return err + } + mb, err := ctx.DataStore.MailboxFor(name) + if err != nil { + // This doesn't indicate not found, likely an IO error + return fmt.Errorf("Failed to get mailbox for %q: %v", name, err) + } + // Delete all messages + err = mb.Purge() + if err != nil { + return fmt.Errorf("Mailbox(%q) purge failed: %v", name, err) + } + log.Tracef("HTTP purged mailbox for %q", name) + + return httpd.RenderJSON(w, "OK") +} + +// MailboxSourceV1 displays the raw source of a message, including headers. Renders text/plain +func MailboxSourceV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { + // Don't have to validate these aren't empty, Gorilla returns 404 + id := ctx.Vars["id"] + name, err := smtpd.ParseMailboxName(ctx.Vars["name"]) + if err != nil { + return err + } + mb, err := ctx.DataStore.MailboxFor(name) + if err != nil { + // This doesn't indicate not found, likely an IO error + return fmt.Errorf("Failed to get mailbox for %q: %v", name, err) + } + message, err := mb.GetMessage(id) + if err == smtpd.ErrNotExist { + http.NotFound(w, req) + return nil + } + if err != nil { + // This doesn't indicate missing, likely an IO error + return fmt.Errorf("GetMessage(%q) failed: %v", id, err) + } + raw, err := message.ReadRaw() + if err != nil { + return fmt.Errorf("ReadRaw(%q) failed: %v", id, err) + } + + w.Header().Set("Content-Type", "text/plain") + if _, err := io.WriteString(w, *raw); err != nil { + return err + } + return nil +} + +// MailboxDeleteV1 removes a particular message from a mailbox. Renders JSON or plain/text OK +func MailboxDeleteV1(w http.ResponseWriter, req *http.Request, ctx *httpd.Context) (err error) { + // Don't have to validate these aren't empty, Gorilla returns 404 + id := ctx.Vars["id"] + name, err := smtpd.ParseMailboxName(ctx.Vars["name"]) + if err != nil { + return err + } + mb, err := ctx.DataStore.MailboxFor(name) + if err != nil { + // This doesn't indicate not found, likely an IO error + return fmt.Errorf("Failed to get mailbox for %q: %v", name, err) + } + message, err := mb.GetMessage(id) + if err == smtpd.ErrNotExist { + http.NotFound(w, req) + return nil + } + if err != nil { + // This doesn't indicate missing, likely an IO error + return fmt.Errorf("GetMessage(%q) failed: %v", id, err) + } + err = message.Delete() + if err != nil { + return fmt.Errorf("Delete(%q) failed: %v", id, err) + } + + return httpd.RenderJSON(w, "OK") +} diff --git a/rest/apiv1_controller_test.go b/rest/apiv1_controller_test.go new file mode 100644 index 0000000..5aedc7e --- /dev/null +++ b/rest/apiv1_controller_test.go @@ -0,0 +1,524 @@ +package rest + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + "net/mail" + "os" + "testing" + "time" + + "github.com/jhillyerd/go.enmime" + "github.com/jhillyerd/inbucket/config" + "github.com/jhillyerd/inbucket/httpd" + "github.com/jhillyerd/inbucket/smtpd" + "github.com/stretchr/testify/mock" +) + +const baseURL = "http://localhost/api/v1" + +type OutputJSONHeader struct { + Mailbox, ID, From, Subject, Date string + Size int +} + +type OutputJSONMessage struct { + Mailbox, ID, From, Subject, Date string + Size int + Header map[string][]string + Body struct { + Text, HTML string + } +} + +type InputMessageData struct { + Mailbox, ID, From, Subject string + Date time.Time + Size int + Header mail.Header + HTML, Text string +} + +func (d *InputMessageData) MockMessage() *MockMessage { + msg := &MockMessage{} + msg.On("ID").Return(d.ID) + msg.On("From").Return(d.From) + msg.On("Subject").Return(d.Subject) + msg.On("Date").Return(d.Date) + msg.On("Size").Return(d.Size) + gomsg := &mail.Message{ + Header: d.Header, + } + msg.On("ReadHeader").Return(gomsg, nil) + body := &enmime.MIMEBody{ + Text: d.Text, + Html: d.HTML, + } + msg.On("ReadBody").Return(body, nil) + return msg +} + +func (d *InputMessageData) CompareToJSONHeader(j *OutputJSONHeader) (errors []string) { + if d.Mailbox != j.Mailbox { + errors = append(errors, fmt.Sprintf("Expected JSON.Mailbox=%q, got %q", d.Mailbox, + j.Mailbox)) + } + if d.ID != j.ID { + errors = append(errors, fmt.Sprintf("Expected JSON.Id=%q, got %q", d.ID, + j.ID)) + } + if d.From != j.From { + errors = append(errors, fmt.Sprintf("Expected JSON.From=%q, got %q", d.From, + j.From)) + } + if d.Subject != j.Subject { + errors = append(errors, fmt.Sprintf("Expected JSON.Subject=%q, got %q", d.Subject, + j.Subject)) + } + exDate := d.Date.Format("2006-01-02T15:04:05.999999999-07:00") + if exDate != j.Date { + errors = append(errors, fmt.Sprintf("Expected JSON.Date=%q, got %q", exDate, + j.Date)) + } + if d.Size != j.Size { + errors = append(errors, fmt.Sprintf("Expected JSON.Size=%v, got %v", d.Size, + j.Size)) + } + + return errors +} + +func (d *InputMessageData) CompareToJSONMessage(j *OutputJSONMessage) (errors []string) { + if d.Mailbox != j.Mailbox { + errors = append(errors, fmt.Sprintf("Expected JSON.Mailbox=%q, got %q", d.Mailbox, + j.Mailbox)) + } + if d.ID != j.ID { + errors = append(errors, fmt.Sprintf("Expected JSON.Id=%q, got %q", d.ID, + j.ID)) + } + if d.From != j.From { + errors = append(errors, fmt.Sprintf("Expected JSON.From=%q, got %q", d.From, + j.From)) + } + if d.Subject != j.Subject { + errors = append(errors, fmt.Sprintf("Expected JSON.Subject=%q, got %q", d.Subject, + j.Subject)) + } + exDate := d.Date.Format("2006-01-02T15:04:05.999999999-07:00") + if exDate != j.Date { + errors = append(errors, fmt.Sprintf("Expected JSON.Date=%q, got %q", exDate, + j.Date)) + } + if d.Size != j.Size { + errors = append(errors, fmt.Sprintf("Expected JSON.Size=%v, got %v", d.Size, + j.Size)) + } + if d.Text != j.Body.Text { + errors = append(errors, fmt.Sprintf("Expected JSON.Text=%q, got %q", d.Text, + j.Body.Text)) + } + if d.HTML != j.Body.HTML { + errors = append(errors, fmt.Sprintf("Expected JSON.Html=%q, got %q", d.HTML, + j.Body.HTML)) + } + for k, vals := range d.Header { + jvals, ok := j.Header[k] + if ok { + for _, v := range vals { + hasValue := false + for _, jv := range jvals { + if v == jv { + hasValue = true + break + } + } + if !hasValue { + errors = append(errors, fmt.Sprintf("JSON.Header[%q] missing value %q", k, v)) + } + } + } else { + errors = append(errors, fmt.Sprintf("JSON.Header missing key %q", k)) + } + } + + return errors +} + +func TestRestMailboxList(t *testing.T) { + // Setup + ds := &MockDataStore{} + logbuf := setupWebServer(ds) + + // Test invalid mailbox name + w, err := testRestGet(baseURL + "/mailbox/foo@bar") + expectCode := 500 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Errorf("Expected code %v, got %v", expectCode, w.Code) + } + + // Test empty mailbox + emptybox := &MockMailbox{} + ds.On("MailboxFor", "empty").Return(emptybox, nil) + emptybox.On("GetMessages").Return([]smtpd.Message{}, nil) + + w, err = testRestGet(baseURL + "/mailbox/empty") + expectCode = 200 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Errorf("Expected code %v, got %v", expectCode, w.Code) + } + + // Test MailboxFor error + ds.On("MailboxFor", "error").Return(&MockMailbox{}, fmt.Errorf("Internal error")) + w, err = testRestGet(baseURL + "/mailbox/error") + expectCode = 500 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Errorf("Expected code %v, got %v", expectCode, w.Code) + } + + 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 MailboxFor error + error2box := &MockMailbox{} + ds.On("MailboxFor", "error2").Return(error2box, nil) + error2box.On("GetMessages").Return([]smtpd.Message{}, fmt.Errorf("Internal error 2")) + + w, err = testRestGet(baseURL + "/mailbox/error2") + expectCode = 500 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Errorf("Expected code %v, got %v", expectCode, w.Code) + } + + // Test JSON message headers + data1 := &InputMessageData{ + Mailbox: "good", + ID: "0001", + From: "from1", + Subject: "subject 1", + Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)), + } + data2 := &InputMessageData{ + Mailbox: "good", + ID: "0002", + From: "from2", + Subject: "subject 2", + Date: time.Date(2012, 7, 1, 10, 11, 12, 253, time.FixedZone("PDT", -700)), + } + goodbox := &MockMailbox{} + ds.On("MailboxFor", "good").Return(goodbox, nil) + msg1 := data1.MockMessage() + msg2 := data2.MockMessage() + goodbox.On("GetMessages").Return([]smtpd.Message{msg1, msg2}, nil) + + // Check return code + w, err = testRestGet(baseURL + "/mailbox/good") + expectCode = 200 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Fatalf("Expected code %v, got %v", expectCode, w.Code) + } + + // Check JSON + dec := json.NewDecoder(w.Body) + var result []OutputJSONHeader + if err := dec.Decode(&result); err != nil { + t.Errorf("Failed to decode JSON: %v", err) + } + if len(result) != 2 { + t.Errorf("Expected 2 results, got %v", len(result)) + } + if errors := data1.CompareToJSONHeader(&result[0]); len(errors) > 0 { + for _, e := range errors { + t.Error(e) + } + } + if errors := data2.CompareToJSONHeader(&result[1]); len(errors) > 0 { + for _, e := range errors { + t.Error(e) + } + } + + 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) + } +} + +func TestRestMessage(t *testing.T) { + // Setup + ds := &MockDataStore{} + logbuf := setupWebServer(ds) + + // Test invalid mailbox name + w, err := testRestGet(baseURL + "/mailbox/foo@bar/0001") + expectCode := 500 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Errorf("Expected code %v, got %v", expectCode, w.Code) + } + + // Test requesting a message that does not exist + emptybox := &MockMailbox{} + ds.On("MailboxFor", "empty").Return(emptybox, nil) + emptybox.On("GetMessage", "0001").Return(&MockMessage{}, smtpd.ErrNotExist) + + w, err = testRestGet(baseURL + "/mailbox/empty/0001") + expectCode = 404 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Errorf("Expected code %v, got %v", expectCode, w.Code) + } + + // Test MailboxFor error + ds.On("MailboxFor", "error").Return(&MockMailbox{}, fmt.Errorf("Internal error")) + w, err = testRestGet(baseURL + "/mailbox/error/0001") + expectCode = 500 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Errorf("Expected code %v, got %v", expectCode, w.Code) + } + + 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 GetMessage error + error2box := &MockMailbox{} + ds.On("MailboxFor", "error2").Return(error2box, nil) + error2box.On("GetMessage", "0001").Return(&MockMessage{}, fmt.Errorf("Internal error 2")) + + w, err = testRestGet(baseURL + "/mailbox/error2/0001") + expectCode = 500 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Errorf("Expected code %v, got %v", expectCode, w.Code) + } + + // Test JSON message headers + data1 := &InputMessageData{ + Mailbox: "good", + ID: "0001", + From: "from1", + Subject: "subject 1", + Date: time.Date(2012, 2, 1, 10, 11, 12, 253, time.FixedZone("PST", -800)), + Header: mail.Header{ + "To": []string{"fred@fish.com", "keyword@nsa.gov"}, + }, + Text: "This is some text", + HTML: "This is some HTML", + } + goodbox := &MockMailbox{} + ds.On("MailboxFor", "good").Return(goodbox, nil) + msg1 := data1.MockMessage() + goodbox.On("GetMessage", "0001").Return(msg1, nil) + + // Check return code + w, err = testRestGet(baseURL + "/mailbox/good/0001") + expectCode = 200 + if err != nil { + t.Fatal(err) + } + if w.Code != expectCode { + t.Fatalf("Expected code %v, got %v", expectCode, w.Code) + } + + // Check JSON + dec := json.NewDecoder(w.Body) + var result OutputJSONMessage + if err := dec.Decode(&result); err != nil { + t.Errorf("Failed to decode JSON: %v", err) + } + if errors := data1.CompareToJSONMessage(&result); len(errors) > 0 { + for _, e := range errors { + t.Error(e) + } + } + + 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) + } +} + +func testRestGet(url string) (*httptest.ResponseRecorder, error) { + req, err := http.NewRequest("GET", url, nil) + req.Header.Add("Accept", "application/json") + if err != nil { + return nil, err + } + + w := httptest.NewRecorder() + httpd.Router.ServeHTTP(w, req) + return w, nil +} + +func setupWebServer(ds smtpd.DataStore) *bytes.Buffer { + // Capture log output + buf := new(bytes.Buffer) + log.SetOutput(buf) + + // Have to reset default mux to prevent duplicate routes + http.DefaultServeMux = http.NewServeMux() + cfg := config.WebConfig{ + TemplateDir: "../themes/integral/templates", + PublicDir: "../themes/integral/public", + } + httpd.Initialize(cfg, ds) + SetupRoutes(httpd.Router) + + return buf +} + +// Mock DataStore object +type MockDataStore struct { + mock.Mock +} + +func (m *MockDataStore) MailboxFor(name string) (smtpd.Mailbox, error) { + args := m.Called(name) + return args.Get(0).(smtpd.Mailbox), args.Error(1) +} + +func (m *MockDataStore) AllMailboxes() ([]smtpd.Mailbox, error) { + args := m.Called() + return args.Get(0).([]smtpd.Mailbox), args.Error(1) +} + +// Mock Mailbox object +type MockMailbox struct { + mock.Mock +} + +func (m *MockMailbox) GetMessages() ([]smtpd.Message, error) { + args := m.Called() + return args.Get(0).([]smtpd.Message), args.Error(1) +} + +func (m *MockMailbox) GetMessage(id string) (smtpd.Message, error) { + args := m.Called(id) + return args.Get(0).(smtpd.Message), args.Error(1) +} + +func (m *MockMailbox) Purge() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockMailbox) NewMessage() (smtpd.Message, error) { + args := m.Called() + return args.Get(0).(smtpd.Message), args.Error(1) +} + +func (m *MockMailbox) String() string { + args := m.Called() + return args.String(0) +} + +// Mock Message object +type MockMessage struct { + mock.Mock +} + +func (m *MockMessage) ID() string { + args := m.Called() + return args.String(0) +} + +func (m *MockMessage) From() string { + args := m.Called() + return args.String(0) +} + +func (m *MockMessage) Date() time.Time { + args := m.Called() + return args.Get(0).(time.Time) +} + +func (m *MockMessage) Subject() string { + args := m.Called() + return args.String(0) +} + +func (m *MockMessage) ReadHeader() (msg *mail.Message, err error) { + args := m.Called() + return args.Get(0).(*mail.Message), args.Error(1) +} + +func (m *MockMessage) ReadBody() (body *enmime.MIMEBody, err error) { + args := m.Called() + return args.Get(0).(*enmime.MIMEBody), args.Error(1) +} + +func (m *MockMessage) ReadRaw() (raw *string, err error) { + args := m.Called() + return args.Get(0).(*string), args.Error(1) +} + +func (m *MockMessage) RawReader() (reader io.ReadCloser, err error) { + args := m.Called() + return args.Get(0).(io.ReadCloser), args.Error(1) +} + +func (m *MockMessage) Size() int64 { + args := m.Called() + return int64(args.Int(0)) +} + +func (m *MockMessage) Append(data []byte) error { + // []byte arg seems to mess up testify/mock + return nil +} + +func (m *MockMessage) Close() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockMessage) Delete() error { + args := m.Called() + return args.Error(0) +} + +func (m *MockMessage) String() string { + args := m.Called() + return args.String(0) +} diff --git a/rest/routes.go b/rest/routes.go new file mode 100644 index 0000000..d474f28 --- /dev/null +++ b/rest/routes.go @@ -0,0 +1,14 @@ +package rest + +import "github.com/gorilla/mux" +import "github.com/jhillyerd/inbucket/httpd" + +// SetupRoutes populates the routes for the REST interface +func SetupRoutes(r *mux.Router) { + // API v1 + r.Path("/api/v1/mailbox/{name}").Handler(httpd.Handler(MailboxListV1)).Name("MailboxListV1").Methods("GET") + r.Path("/api/v1/mailbox/{name}").Handler(httpd.Handler(MailboxPurgeV1)).Name("MailboxPurgeV1").Methods("DELETE") + r.Path("/api/v1/mailbox/{name}/{id}").Handler(httpd.Handler(MailboxShowV1)).Name("MailboxShowV1").Methods("GET") + r.Path("/api/v1/mailbox/{name}/{id}").Handler(httpd.Handler(MailboxDeleteV1)).Name("MailboxDeleteV1").Methods("DELETE") + r.Path("/api/v1/mailbox/{name}/{id}/source").Handler(httpd.Handler(MailboxSourceV1)).Name("MailboxSourceV1").Methods("GET") +}