Files
warmbly/internal/api/handler/unibox_forward_test.go
T

155 lines
5.4 KiB
Go

package handler
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/api/middleware"
"github.com/warmbly/warmbly/internal/app/audit"
"github.com/warmbly/warmbly/internal/app/emailsend"
"github.com/warmbly/warmbly/internal/app/unibox"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
)
type fakeForwardUnibox struct {
unibox.UniboxService
mailbox uuid.UUID
read int
}
func (f *fakeForwardUnibox) ForwardSource(context.Context, uuid.UUID, uuid.UUID) (*models.ForwardedMessage, *errx.Error) {
f.read++
return &models.ForwardedMessage{EmailID: f.mailbox, Subject: "Pricing"}, nil
}
type fakeForwardSend struct{ req *emailsend.SendEmailRequest }
func (f *fakeForwardSend) SendEmail(_ context.Context, _, _, _ uuid.UUID, req *emailsend.SendEmailRequest) (*emailsend.SendEmailResponse, *errx.Error) {
f.req = req
return &emailsend.SendEmailResponse{SendMode: "instant"}, nil
}
// Forwarding discloses a stored message, so a key that cannot read the inbox
// is refused before the message is read, and a restricted key must be allowed
// the message's mailbox as well as the sending one.
func TestUniboxReplyForwardNeedsReadAccess(t *testing.T) {
gin.SetMode(gin.TestMode)
sender, source := uuid.New(), uuid.New()
for _, tc := range []struct {
name string
perms uint64
allowed []uuid.UUID
status int
read bool
}{
{"write-only key", models.APIPermWriteUnibox, nil, http.StatusForbidden, false},
{"read and write key", models.APIPermWriteUnibox | models.APIPermReadUnibox, nil, http.StatusOK, true},
{"key not allowed the source mailbox", models.APIPermWriteUnibox | models.APIPermReadUnibox, []uuid.UUID{sender}, http.StatusForbidden, true},
} {
t.Run(tc.name, func(t *testing.T) {
ub := &fakeForwardUnibox{mailbox: source}
send := &fakeForwardSend{}
h := &Handler{UniboxService: ub, EmailSendService: send, AuditService: audit.NewNoOpService()}
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set(middleware.UserIDKey, uuid.NewString())
c.Set(middleware.OrganizationIDKey, uuid.New())
c.Set(middleware.AuthTypeKey, middleware.AuthTypeAPIKey)
c.Set(middleware.APIKeyPermissionsKey, tc.perms)
if tc.allowed != nil {
c.Set(middleware.APIKeyAllowedEmailAccountsKey, tc.allowed)
}
})
router.POST("/unibox/reply", h.UniboxReply)
body := `{"email_account_id":"` + sender.String() + `","to":["new@example.com"],"subject":"Fwd: Pricing","forward_message_id":"` + uuid.NewString() + `"}`
req := httptest.NewRequest(http.MethodPost, "/unibox/reply", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != tc.status {
t.Fatalf("status %d, want %d: %s", rec.Code, tc.status, rec.Body.String())
}
if (ub.read > 0) != tc.read {
t.Fatalf("message read %d times, want read=%v", ub.read, tc.read)
}
if tc.status == http.StatusOK && (send.req == nil || send.req.Forward == nil || send.req.Forward.Subject != "Pricing") {
t.Fatalf("forward did not reach the send: %+v", send.req)
}
if tc.status != http.StatusOK && send.req != nil {
t.Fatal("a refused forward was sent")
}
})
}
}
type fakeForwardThreadUnibox struct {
fakeForwardUnibox
parentLookups int
}
func (f *fakeForwardThreadUnibox) LatestMessageIDInThread(context.Context, uuid.UUID, string) (string, *errx.Error) {
f.parentLookups++
return "<parent@example.com>", nil
}
// A forward from an open conversation answers nobody, so no In-Reply-To is
// resolved from the thread it was forwarded from; a reply still gets one.
func TestUniboxReplyForwardFromAThreadIsNoReply(t *testing.T) {
gin.SetMode(gin.TestMode)
sender := uuid.New()
for _, tc := range []struct {
name string
forward bool
parent bool
}{
{"forward", true, false},
{"reply", false, true},
} {
t.Run(tc.name, func(t *testing.T) {
ub := &fakeForwardThreadUnibox{fakeForwardUnibox: fakeForwardUnibox{mailbox: sender}}
send := &fakeForwardSend{}
h := &Handler{UniboxService: ub, EmailSendService: send, AuditService: audit.NewNoOpService()}
router := gin.New()
router.Use(func(c *gin.Context) {
c.Set(middleware.UserIDKey, uuid.NewString())
c.Set(middleware.OrganizationIDKey, uuid.New())
c.Set(middleware.AuthTypeKey, middleware.AuthTypeAPIKey)
c.Set(middleware.APIKeyPermissionsKey, models.APIPermWriteUnibox|models.APIPermReadUnibox)
})
router.POST("/unibox/reply", h.UniboxReply)
body := `{"email_account_id":"` + sender.String() + `","to":["team@example.com"],"subject":"Fwd: Pricing","body_plain":"FYI","thread_id":"conversation-1"`
if tc.forward {
body += `,"forward_message_id":"` + uuid.NewString() + `"`
}
body += `}`
req := httptest.NewRequest(http.MethodPost, "/unibox/reply", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
}
if got := len(send.req.InReplyTo) > 0; got != tc.parent {
t.Fatalf("in_reply_to %v, want a parent: %v", send.req.InReplyTo, tc.parent)
}
if (ub.parentLookups > 0) != tc.parent {
t.Fatalf("parent looked up %d times", ub.parentLookups)
}
})
}
}