- Updated environment configuration to unify frontend for mail and drive under a single service. - Revised README to reflect changes in frontend setup and routing for the unified application. - Introduced new API documentation endpoints for better accessibility of API specifications. - Enhanced drive and mail services with improved handling of file uploads and metadata enrichment. - Implemented new API token management features, including creation, listing, and revocation of tokens. - Added tests for new functionalities in drive and mail services to ensure reliability and correctness.
1190 lines
35 KiB
Go
1190 lines
35 KiB
Go
package mail
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/middleware"
|
|
"github.com/ultisuite/ulti-backend/internal/api/query"
|
|
"github.com/ultisuite/ulti-backend/internal/auth"
|
|
"github.com/ultisuite/ulti-backend/internal/mail/credentials"
|
|
"github.com/ultisuite/ulti-backend/internal/mail/listunsubscribe"
|
|
"github.com/ultisuite/ulti-backend/internal/mail/rules"
|
|
)
|
|
|
|
const (
|
|
testExternalID = "ext-user-1"
|
|
testExternalID2 = "ext-user-2"
|
|
testUserID = "user-uuid-1"
|
|
testMailAccountID = "550e8400-e29b-41d4-a716-446655440000"
|
|
)
|
|
|
|
type fakeMailService struct {
|
|
messages map[string]map[string]any
|
|
deleted map[string]bool
|
|
sent []sendMessageRequest
|
|
}
|
|
|
|
func newFakeMailService() *fakeMailService {
|
|
return &fakeMailService{
|
|
messages: map[string]map[string]any{
|
|
"msg-1": {
|
|
"id": "msg-1", "subject": "Hello", "snippet": "Preview text",
|
|
"flags": []string{"unread"}, "labels": []string{"inbox"},
|
|
},
|
|
},
|
|
deleted: make(map[string]bool),
|
|
}
|
|
}
|
|
|
|
func (f *fakeMailService) ResolveUserID(_ context.Context, externalID string) (string, error) {
|
|
if externalID != testExternalID {
|
|
return "", ErrUserNotProvisioned
|
|
}
|
|
return testUserID, nil
|
|
}
|
|
|
|
func (f *fakeMailService) GetMailSettings(_ context.Context, externalID string) (MailSettings, error) {
|
|
if externalID != testExternalID {
|
|
return MailSettings{}, ErrUserNotProvisioned
|
|
}
|
|
return defaultMailSettings(), nil
|
|
}
|
|
|
|
func (f *fakeMailService) UpdateMailSettings(_ context.Context, externalID string, req *patchMailSettingsRequest) (MailSettings, error) {
|
|
if externalID != testExternalID {
|
|
return MailSettings{}, ErrUserNotProvisioned
|
|
}
|
|
current := defaultMailSettings()
|
|
if req.Density != nil {
|
|
current.Density = *req.Density
|
|
}
|
|
if req.ThemeMode != nil {
|
|
current.ThemeMode = *req.ThemeMode
|
|
}
|
|
if req.BackgroundID != nil {
|
|
current.BackgroundID = *req.BackgroundID
|
|
}
|
|
if req.InboxSort != nil {
|
|
current.InboxSort = *req.InboxSort
|
|
}
|
|
if req.ReadingPane != nil {
|
|
current.ReadingPane = *req.ReadingPane
|
|
}
|
|
if req.ConversationMode != nil {
|
|
current.ConversationMode = *req.ConversationMode
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListUnifiedFolders(_ context.Context, externalID, _ string, params query.ListParams) (UnifiedFoldersList, error) {
|
|
if externalID != testExternalID {
|
|
return UnifiedFoldersList{}, ErrUserNotProvisioned
|
|
}
|
|
total := int64(0)
|
|
return UnifiedFoldersList{Pagination: params.Meta(&total)}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) CreateUnifiedFolder(_ context.Context, _ string, _ *createUnifiedFolderRequest) (string, error) {
|
|
return "uf-1", nil
|
|
}
|
|
|
|
func (f *fakeMailService) UpdateUnifiedFolder(_ context.Context, externalID, _ string, _ *updateUnifiedFolderRequest) error {
|
|
if externalID != testExternalID {
|
|
return ErrUserNotProvisioned
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) DeleteUnifiedFolder(_ context.Context, externalID, _ string) error {
|
|
if externalID != testExternalID {
|
|
return ErrUserNotProvisioned
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListMessages(_ context.Context, externalID string, _ MessageListFilter, params query.ListParams) (MessagesList, error) {
|
|
if externalID != testExternalID {
|
|
return MessagesList{}, ErrUserNotProvisioned
|
|
}
|
|
msgs := make([]map[string]any, 0, len(f.messages))
|
|
for id, msg := range f.messages {
|
|
if f.deleted[id] {
|
|
continue
|
|
}
|
|
msgs = append(msgs, msg)
|
|
}
|
|
total := int64(len(msgs))
|
|
return MessagesList{
|
|
Messages: msgs,
|
|
Page: params.Page,
|
|
Pagination: params.Meta(&total),
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) SendMailtoListUnsubscribe(context.Context, string, string, MailSender) (*listunsubscribe.Mailto, error) {
|
|
return nil, ErrListUnsubscribeUnavailable
|
|
}
|
|
|
|
func (f *fakeMailService) GetMessage(_ context.Context, externalID, messageID string) (map[string]any, error) {
|
|
if externalID != testExternalID {
|
|
return nil, ErrUserNotProvisioned
|
|
}
|
|
if f.deleted[messageID] {
|
|
return nil, ErrNotFound
|
|
}
|
|
msg, ok := f.messages[messageID]
|
|
if !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
return msg, nil
|
|
}
|
|
|
|
func (f *fakeMailService) UpdateLabels(_ context.Context, externalID, messageID string, labels []string) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
if f.deleted[messageID] {
|
|
return ErrNotFound
|
|
}
|
|
msg, ok := f.messages[messageID]
|
|
if !ok {
|
|
return ErrNotFound
|
|
}
|
|
msg["labels"] = labels
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) UpdateFlags(_ context.Context, externalID, messageID string, flags []string) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
if f.deleted[messageID] {
|
|
return ErrNotFound
|
|
}
|
|
msg, ok := f.messages[messageID]
|
|
if !ok {
|
|
return ErrNotFound
|
|
}
|
|
msg["flags"] = flags
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) DeleteMessage(_ context.Context, externalID, messageID string) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
if _, ok := f.messages[messageID]; !ok || f.deleted[messageID] {
|
|
return ErrNotFound
|
|
}
|
|
f.deleted[messageID] = true
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) SendMessage(_ context.Context, userID string, req *sendMessageRequest) (string, string, error) {
|
|
if userID != testUserID {
|
|
return "", "", ErrAccountNotFound
|
|
}
|
|
f.sent = append(f.sent, *req)
|
|
return "outbox-1", "queued", nil
|
|
}
|
|
|
|
func (f *fakeMailService) SendOutboxNow(context.Context, string, string) (string, error) {
|
|
return "", ErrNotFound
|
|
}
|
|
|
|
func (f *fakeMailService) RescheduleOutbox(context.Context, string, string, time.Time) (string, error) {
|
|
return "", ErrNotFound
|
|
}
|
|
|
|
func (f *fakeMailService) CancelScheduledOutbox(context.Context, string, string) (string, error) {
|
|
return "", ErrNotFound
|
|
}
|
|
|
|
type outboxFakeService struct {
|
|
fakeMailService
|
|
items map[string]string
|
|
}
|
|
|
|
func newOutboxFakeService() *outboxFakeService {
|
|
return &outboxFakeService{
|
|
fakeMailService: *newFakeMailService(),
|
|
items: map[string]string{
|
|
"outbox-scheduled": "scheduled",
|
|
"outbox-queued": "queued",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (f *outboxFakeService) SendOutboxNow(_ context.Context, userID, outboxID string) (string, error) {
|
|
if userID != testUserID {
|
|
return "", ErrNotFound
|
|
}
|
|
status, ok := f.items[outboxID]
|
|
if !ok {
|
|
return "", ErrNotFound
|
|
}
|
|
if status != "scheduled" {
|
|
return "", ErrInvalidOutboxStatus
|
|
}
|
|
f.items[outboxID] = "queued"
|
|
return "queued", nil
|
|
}
|
|
|
|
func (f *outboxFakeService) RescheduleOutbox(_ context.Context, userID, outboxID string, _ time.Time) (string, error) {
|
|
if userID != testUserID {
|
|
return "", ErrNotFound
|
|
}
|
|
status, ok := f.items[outboxID]
|
|
if !ok {
|
|
return "", ErrNotFound
|
|
}
|
|
if status != "scheduled" && status != "queued" {
|
|
return "", ErrInvalidOutboxStatus
|
|
}
|
|
f.items[outboxID] = "scheduled"
|
|
return "scheduled", nil
|
|
}
|
|
|
|
func (f *outboxFakeService) CancelScheduledOutbox(_ context.Context, userID, outboxID string) (string, error) {
|
|
if userID != testUserID {
|
|
return "", ErrNotFound
|
|
}
|
|
status, ok := f.items[outboxID]
|
|
if !ok {
|
|
return "", ErrNotFound
|
|
}
|
|
if status != "scheduled" {
|
|
return "", ErrInvalidOutboxStatus
|
|
}
|
|
f.items[outboxID] = "cancelled"
|
|
return "cancelled", nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListDrafts(context.Context, string, query.ListParams) (DraftsList, error) {
|
|
return DraftsList{}, nil
|
|
}
|
|
func (f *fakeMailService) GetDraft(context.Context, string, string) (map[string]any, error) {
|
|
return nil, ErrNotFound
|
|
}
|
|
func (f *fakeMailService) CreateDraft(context.Context, string, *draftRequest) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (f *fakeMailService) UpdateDraft(context.Context, string, string, *draftRequest) error {
|
|
return nil
|
|
}
|
|
func (f *fakeMailService) DeleteDraft(context.Context, string, string) error {
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListAccounts(context.Context, string, query.ListParams) (AccountsList, error) {
|
|
return AccountsList{}, nil
|
|
}
|
|
func (f *fakeMailService) CreateAccount(context.Context, string, *createAccountRequest) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (f *fakeMailService) CreateAccountWithCredential(context.Context, string, *createAccountRequest, credentials.Credential) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (f *fakeMailService) GetAccount(context.Context, string, string) (map[string]any, error) {
|
|
return nil, ErrNotFound
|
|
}
|
|
func (f *fakeMailService) UpdateAccount(context.Context, string, string, *updateAccountRequest) error {
|
|
return nil
|
|
}
|
|
func (f *fakeMailService) CredentialForConnectionTest(context.Context, string, *testAccountRequest) (credentials.Credential, error) {
|
|
return credentials.Credential{AuthType: credentials.AuthPassword, Username: "u", Password: "p"}, nil
|
|
}
|
|
func (f *fakeMailService) DeleteAccount(context.Context, string, string) error { return nil }
|
|
func (f *fakeMailService) ResanitizeAccountBodies(context.Context, string, string) (ResanitizeBodiesResult, error) {
|
|
return ResanitizeBodiesResult{}, nil
|
|
}
|
|
func (f *fakeMailService) MessageAccountID(_ context.Context, _, messageID string) (string, error) {
|
|
if f.deleted[messageID] {
|
|
return "", ErrNotFound
|
|
}
|
|
if _, ok := f.messages[messageID]; !ok {
|
|
return "", ErrNotFound
|
|
}
|
|
return testMailAccountID, nil
|
|
}
|
|
func (f *fakeMailService) AttachmentAccountID(context.Context, string, string) (string, error) {
|
|
return testMailAccountID, nil
|
|
}
|
|
func (f *fakeMailService) ThreadAccessible(context.Context, string, string, []string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
func (f *fakeMailService) GetThread(context.Context, string, string, []string) (map[string]any, error) {
|
|
return map[string]any{"messages": []any{}}, nil
|
|
}
|
|
func (f *fakeMailService) ListRules(context.Context, string, query.ListParams) (RulesList, error) {
|
|
return RulesList{}, nil
|
|
}
|
|
func (f *fakeMailService) CreateRule(context.Context, string, *createRuleRequest) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (f *fakeMailService) UpdateRule(_ context.Context, externalID, ruleID string, _ *updateRuleRequest) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
func (f *fakeMailService) DeleteRule(_ context.Context, externalID, ruleID string) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) SimulateRule(_ context.Context, externalID string, req *simulateRuleRequest) (any, error) {
|
|
if externalID != testExternalID {
|
|
return rules.SimulationResult{}, ErrUserNotProvisioned
|
|
}
|
|
if req.Message == nil {
|
|
return rules.SimulationResult{}, ErrNotFound
|
|
}
|
|
|
|
matched := false
|
|
for _, cond := range []struct {
|
|
field, operator, value string
|
|
}{
|
|
{"from", "contains", "alice"},
|
|
{"subject", "contains", "invoice"},
|
|
} {
|
|
fieldValue := ""
|
|
switch cond.field {
|
|
case "from":
|
|
fieldValue = req.Message.From
|
|
case "subject":
|
|
fieldValue = req.Message.Subject
|
|
}
|
|
if strings.Contains(strings.ToLower(fieldValue), strings.ToLower(cond.value)) {
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !matched {
|
|
return rules.SimulationResult{Matched: false}, nil
|
|
}
|
|
|
|
return rules.SimulationResult{
|
|
Matched: true,
|
|
Actions: []rules.SimulatedActionResult{
|
|
{ActionResult: rules.ActionResult{Type: "label", Value: "work", OK: true}},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListWebhooks(context.Context, string, query.ListParams) (WebhooksList, error) {
|
|
return WebhooksList{}, nil
|
|
}
|
|
func (f *fakeMailService) CreateWebhook(context.Context, string, *createWebhookRequest, string, int) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (f *fakeMailService) UpdateWebhook(context.Context, string, string, *updateWebhookRequest, string, int) error {
|
|
return nil
|
|
}
|
|
func (f *fakeMailService) PreviewWebhookTemplate(_ context.Context, _ string, req *previewWebhookRequest) (map[string]any, error) {
|
|
return map[string]any{"payload": req.BodyTemplate}, nil
|
|
}
|
|
func (f *fakeMailService) DeleteWebhook(_ context.Context, externalID, webhookID string) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListIdentities(_ context.Context, externalID, accountID string, params query.ListParams) (IdentitiesList, error) {
|
|
if externalID != testExternalID {
|
|
return IdentitiesList{}, ErrAccountNotFound
|
|
}
|
|
if accountID != testMailAccountID {
|
|
return IdentitiesList{}, ErrAccountNotFound
|
|
}
|
|
total := int64(1)
|
|
return IdentitiesList{
|
|
Identities: []map[string]any{{
|
|
"id": "id-1", "account_id": testMailAccountID, "email": "sender@example.com",
|
|
"name": "Sender", "is_default": true, "signature_html": "",
|
|
"reply_to_addrs": []string{}, "created_at": nil, "updated_at": nil,
|
|
}},
|
|
Pagination: params.Meta(&total),
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) GetIdentity(_ context.Context, externalID, identityID string) (map[string]any, error) {
|
|
if externalID != testExternalID || identityID != "id-1" {
|
|
return nil, ErrNotFound
|
|
}
|
|
return map[string]any{
|
|
"id": "id-1", "account_id": testMailAccountID, "email": "sender@example.com",
|
|
"name": "Sender", "is_default": true, "signature_html": "",
|
|
"reply_to_addrs": []string{}, "created_at": nil, "updated_at": nil,
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) CreateIdentity(_ context.Context, externalID, accountID string, req *createIdentityRequest) (string, error) {
|
|
if externalID != testExternalID || accountID != testMailAccountID {
|
|
return "", ErrAccountNotFound
|
|
}
|
|
if req.Email == "" {
|
|
return "", ErrAccountNotFound
|
|
}
|
|
return "id-new", nil
|
|
}
|
|
|
|
func (f *fakeMailService) UpdateIdentity(_ context.Context, externalID, identityID string, _ *updateIdentityRequest) error {
|
|
if externalID != testExternalID || identityID != "id-1" {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) DeleteIdentity(_ context.Context, externalID, identityID string) error {
|
|
if externalID != testExternalID || identityID != "id-1" {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListSignatures(_ context.Context, externalID string, params query.ListParams) (SignaturesList, error) {
|
|
if externalID != testExternalID {
|
|
return SignaturesList{}, ErrNotFound
|
|
}
|
|
total := int64(0)
|
|
return SignaturesList{Signatures: []map[string]any{}, Pagination: params.Meta(&total)}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) GetSignature(_ context.Context, externalID, signatureID string) (map[string]any, error) {
|
|
if externalID != testExternalID {
|
|
return nil, ErrNotFound
|
|
}
|
|
return map[string]any{"id": signatureID, "name": "Sig", "html": "<p>Hi</p>", "sort_order": 0}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) CreateSignature(_ context.Context, externalID string, req *createSignatureRequest) (string, error) {
|
|
if externalID != testExternalID || req.Name == "" {
|
|
return "", ErrNotFound
|
|
}
|
|
return "sig-new", nil
|
|
}
|
|
|
|
func (f *fakeMailService) UpdateSignature(_ context.Context, externalID, signatureID string, _ *updateSignatureRequest) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) DeleteSignature(_ context.Context, externalID, signatureID string) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListFolders(_ context.Context, externalID, accountID string, params query.ListParams) (FoldersList, error) {
|
|
if externalID != testExternalID {
|
|
return FoldersList{}, ErrAccountNotFound
|
|
}
|
|
total := int64(0)
|
|
return FoldersList{Folders: []map[string]any{}, Pagination: params.Meta(&total)}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) GetFolder(_ context.Context, externalID, folderID string) (map[string]any, error) {
|
|
if externalID != testExternalID {
|
|
return nil, ErrNotFound
|
|
}
|
|
return map[string]any{"id": folderID, "name": "Inbox", "remote_name": "INBOX", "folder_type": "inbox"}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) CreateFolder(_ context.Context, userID string, _ *createFolderRequest) (string, error) {
|
|
if userID != testUserID {
|
|
return "", ErrAccountNotFound
|
|
}
|
|
return "folder-1", nil
|
|
}
|
|
|
|
func (f *fakeMailService) UpdateFolder(_ context.Context, externalID, folderID string, _ *updateFolderRequest) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) DeleteFolder(_ context.Context, externalID, folderID string) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListUserLabels(_ context.Context, externalID string, params query.ListParams) (UserLabelsList, error) {
|
|
if externalID != testExternalID {
|
|
return UserLabelsList{}, nil
|
|
}
|
|
total := int64(0)
|
|
return UserLabelsList{Labels: []map[string]any{}, Pagination: params.Meta(&total)}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) CreateUserLabel(_ context.Context, externalID string, _ *createUserLabelRequest) (string, error) {
|
|
if externalID != testExternalID {
|
|
return "", ErrNotFound
|
|
}
|
|
return "label-1", nil
|
|
}
|
|
|
|
func (f *fakeMailService) UpdateUserLabel(_ context.Context, externalID, labelID string, _ *updateUserLabelRequest) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) DeleteUserLabel(_ context.Context, externalID, labelID string) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) ReorderUserLabels(_ context.Context, externalID string, _ *reorderLabelsRequest) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) ReorderUnifiedFolders(_ context.Context, externalID string, _ *reorderUnifiedFoldersRequest) error {
|
|
if externalID != testExternalID {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMailService) SearchMessages(_ context.Context, externalID string, _ MessageSearchFilter, params query.ListParams) (MessageSearchResult, error) {
|
|
if externalID != testExternalID {
|
|
return MessageSearchResult{}, ErrUserNotProvisioned
|
|
}
|
|
msgs := make([]map[string]any, 0, len(f.messages))
|
|
for id, msg := range f.messages {
|
|
if f.deleted[id] {
|
|
continue
|
|
}
|
|
msgs = append(msgs, msg)
|
|
}
|
|
total := int64(len(msgs))
|
|
return MessageSearchResult{Messages: msgs, Pagination: params.Meta(&total)}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) ListMessageAttachments(_ context.Context, externalID, messageID string) ([]map[string]any, error) {
|
|
if externalID != testExternalID {
|
|
return nil, ErrNotFound
|
|
}
|
|
if _, ok := f.messages[messageID]; !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
return []map[string]any{}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) MessageAttachmentCIDMap(_ context.Context, externalID, messageID string) (map[string]string, error) {
|
|
if externalID != testExternalID {
|
|
return nil, ErrNotFound
|
|
}
|
|
if _, ok := f.messages[messageID]; !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
return map[string]string{}, nil
|
|
}
|
|
|
|
func (f *fakeMailService) UploadMessageAttachment(context.Context, string, string, string, string, string, bool, io.Reader, int64) (string, error) {
|
|
return "att-1", nil
|
|
}
|
|
|
|
func (f *fakeMailService) OpenAttachment(context.Context, string, string) (string, string, int64, bool, io.ReadCloser, error) {
|
|
return "", "", 0, false, nil, ErrAttachmentNotFound
|
|
}
|
|
|
|
func (f *fakeMailService) UploadDraftAttachment(context.Context, string, string, string, string, string, bool, io.Reader, int64) (string, error) {
|
|
return "att-draft-1", nil
|
|
}
|
|
|
|
func (f *fakeMailService) OpenDraftAttachment(context.Context, string, string, string) (string, string, io.ReadCloser, error) {
|
|
return "", "", nil, ErrAttachmentNotFound
|
|
}
|
|
|
|
func (f *fakeMailService) SaveAttachmentToDrive(context.Context, string, string, string, string, string, string, string) (string, error) {
|
|
return "/Ultimail/test.pdf", nil
|
|
}
|
|
|
|
func (f *fakeMailService) SaveMessageAttachmentsToDrive(context.Context, string, string, string, string, string, string) ([]map[string]any, error) {
|
|
return []map[string]any{}, nil
|
|
}
|
|
|
|
func newTestMailRouter(svc ServiceAPI) http.Handler {
|
|
return newTestMailRouterWithClaims(svc, &auth.Claims{
|
|
Sub: testExternalID,
|
|
Email: "user@example.com",
|
|
})
|
|
}
|
|
|
|
func newTestMailRouterWithClaims(svc ServiceAPI, claims *auth.Claims) http.Handler {
|
|
h := NewHandlerWithService(svc)
|
|
r := chi.NewRouter()
|
|
r.Use(middleware.WithTestClaims(claims))
|
|
r.Mount("/", h.Routes())
|
|
return r
|
|
}
|
|
|
|
func TestListMessages(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/messages", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var body MessagesList
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if len(body.Messages) != 1 {
|
|
t.Fatalf("messages len = %d, want 1", len(body.Messages))
|
|
}
|
|
if body.Messages[0]["id"] != "msg-1" {
|
|
t.Fatalf("message id = %v", body.Messages[0]["id"])
|
|
}
|
|
}
|
|
|
|
func TestGetMessage(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
t.Run("found", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/messages/msg-1", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var body map[string]any
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if body["id"] != "msg-1" {
|
|
t.Fatalf("id = %v", body["id"])
|
|
}
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/messages/missing", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSendMessage(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
payload := map[string]any{
|
|
"account_id": testMailAccountID,
|
|
"to": []string{"recipient@example.com"},
|
|
"subject": "Test subject",
|
|
"body_text": "Hello world",
|
|
}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/send", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusAccepted {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusAccepted, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if resp["id"] != "outbox-1" || resp["status"] != "queued" {
|
|
t.Fatalf("response = %#v", resp)
|
|
}
|
|
if len(svc.sent) != 1 {
|
|
t.Fatalf("sent count = %d, want 1", len(svc.sent))
|
|
}
|
|
if svc.sent[0].Subject != "Test subject" {
|
|
t.Fatalf("sent subject = %q", svc.sent[0].Subject)
|
|
}
|
|
}
|
|
|
|
func TestUpdateLabels(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
body, err := json.Marshal(map[string]any{"labels": []string{"work", "important"}})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/messages/msg-1/labels", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusNoContent, rec.Body.String())
|
|
}
|
|
|
|
labels, ok := svc.messages["msg-1"]["labels"].([]string)
|
|
if !ok {
|
|
t.Fatalf("labels type = %T", svc.messages["msg-1"]["labels"])
|
|
}
|
|
if len(labels) != 2 || labels[0] != "work" || labels[1] != "important" {
|
|
t.Fatalf("labels = %#v", labels)
|
|
}
|
|
}
|
|
|
|
func TestUpdateFlags(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
body, err := json.Marshal(map[string]any{"flags": []string{"read", "starred"}})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/messages/msg-1/flags", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusNoContent, rec.Body.String())
|
|
}
|
|
|
|
flags, ok := svc.messages["msg-1"]["flags"].([]string)
|
|
if !ok {
|
|
t.Fatalf("flags type = %T", svc.messages["msg-1"]["flags"])
|
|
}
|
|
if len(flags) != 2 || flags[0] != "read" || flags[1] != "starred" {
|
|
t.Fatalf("flags = %#v", flags)
|
|
}
|
|
}
|
|
|
|
func TestUpdateLabelsCrossUser(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouterWithClaims(svc, &auth.Claims{
|
|
Sub: testExternalID2,
|
|
Email: "other@example.com",
|
|
})
|
|
|
|
body, err := json.Marshal(map[string]any{"labels": []string{"stolen"}})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPut, "/messages/msg-1/labels", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDeleteMessage(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/messages/msg-1", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusNoContent, rec.Body.String())
|
|
}
|
|
if !svc.deleted["msg-1"] {
|
|
t.Fatal("message was not marked deleted")
|
|
}
|
|
|
|
// Subsequent get returns 404.
|
|
req = httptest.NewRequest(http.MethodGet, "/messages/msg-1", nil)
|
|
rec = httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("get after delete status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestSendOutboxNow(t *testing.T) {
|
|
svc := newOutboxFakeService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
t.Run("happy path", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/outbox-scheduled/send-now", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusAccepted {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusAccepted, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if resp["id"] != "outbox-scheduled" || resp["status"] != "queued" {
|
|
t.Fatalf("response = %#v", resp)
|
|
}
|
|
if svc.items["outbox-scheduled"] != "queued" {
|
|
t.Fatalf("item status = %q, want queued", svc.items["outbox-scheduled"])
|
|
}
|
|
})
|
|
|
|
t.Run("invalid status", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/outbox-queued/send-now", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusConflict {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusConflict, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/missing/send-now", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRescheduleOutbox(t *testing.T) {
|
|
svc := newOutboxFakeService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
future := time.Now().Add(2 * time.Hour).UTC().Format(time.RFC3339)
|
|
|
|
t.Run("happy path", func(t *testing.T) {
|
|
body, err := json.Marshal(map[string]string{"schedule_at": future})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/outbox-scheduled/reschedule", bytes.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, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if resp["status"] != "scheduled" {
|
|
t.Fatalf("response = %#v", resp)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid schedule_at", func(t *testing.T) {
|
|
body, err := json.Marshal(map[string]string{"schedule_at": "not-a-date"})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/outbox-scheduled/reschedule", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("invalid status", func(t *testing.T) {
|
|
svc.items["outbox-sent"] = "sent"
|
|
body, err := json.Marshal(map[string]string{"schedule_at": future})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/outbox-sent/reschedule", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusConflict {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusConflict, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCancelScheduledOutbox(t *testing.T) {
|
|
svc := newOutboxFakeService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
t.Run("happy path", func(t *testing.T) {
|
|
svc.items["outbox-cancel-me"] = "scheduled"
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/outbox-cancel-me/cancel", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp map[string]string
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if resp["status"] != "cancelled" {
|
|
t.Fatalf("response = %#v", resp)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid status", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/outbox-queued/cancel", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusConflict {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusConflict, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("not found", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/outbox/missing/cancel", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusNotFound, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSimulateRule(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
t.Run("matched inline rule", func(t *testing.T) {
|
|
body, err := json.Marshal(map[string]any{
|
|
"message": map[string]any{
|
|
"from": "Alice <alice@example.com>",
|
|
"to": []string{"bob@example.com"},
|
|
"subject": "Invoice Q1",
|
|
},
|
|
"rule": map[string]any{
|
|
"conditions": []map[string]string{
|
|
{"field": "subject", "operator": "contains", "value": "invoice"},
|
|
},
|
|
"actions": []map[string]string{
|
|
{"type": "label", "value": "work"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/rules/simulate", bytes.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, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp rules.SimulationResult
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if !resp.Matched {
|
|
t.Fatal("matched = false, want true")
|
|
}
|
|
if len(resp.Actions) != 1 || resp.Actions[0].Type != "label" || !resp.Actions[0].OK {
|
|
t.Fatalf("actions = %#v", resp.Actions)
|
|
}
|
|
})
|
|
|
|
t.Run("no match", func(t *testing.T) {
|
|
body, err := json.Marshal(map[string]any{
|
|
"message": map[string]any{
|
|
"from": "bob@example.com",
|
|
"subject": "Hello",
|
|
},
|
|
"rule": map[string]any{
|
|
"conditions": []map[string]string{
|
|
{"field": "subject", "operator": "contains", "value": "invoice"},
|
|
},
|
|
"actions": []map[string]string{
|
|
{"type": "label", "value": "work"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/rules/simulate", bytes.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, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var resp rules.SimulationResult
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if resp.Matched {
|
|
t.Fatal("matched = true, want false")
|
|
}
|
|
})
|
|
|
|
t.Run("validation missing message", func(t *testing.T) {
|
|
body, err := json.Marshal(map[string]any{
|
|
"rule": map[string]any{
|
|
"conditions": []map[string]string{
|
|
{"field": "subject", "operator": "contains", "value": "invoice"},
|
|
},
|
|
"actions": []map[string]string{
|
|
{"type": "label", "value": "work"},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/rules/simulate", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("validation missing rule source", func(t *testing.T) {
|
|
body, err := json.Marshal(map[string]any{
|
|
"message": map[string]any{
|
|
"subject": "Invoice Q1",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/rules/simulate", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMailSettingsHandlers(t *testing.T) {
|
|
svc := newFakeMailService()
|
|
router := newTestMailRouter(svc)
|
|
|
|
t.Run("get defaults", func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/settings", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var body MailSettings
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if body.Density != "default" || body.ThemeMode != "system" {
|
|
t.Fatalf("body = %#v", body)
|
|
}
|
|
})
|
|
|
|
t.Run("patch density", func(t *testing.T) {
|
|
payload, err := json.Marshal(map[string]string{"density": "compact"})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPatch, "/settings", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
|
|
var body MailSettings
|
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode body: %v", err)
|
|
}
|
|
if body.Density != "compact" {
|
|
t.Fatalf("density = %q, want compact", body.Density)
|
|
}
|
|
})
|
|
|
|
t.Run("patch invalid", func(t *testing.T) {
|
|
payload, err := json.Marshal(map[string]string{"density": "invalid"})
|
|
if err != nil {
|
|
t.Fatalf("marshal payload: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPatch, "/settings", bytes.NewReader(payload))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d; body = %s", rec.Code, http.StatusBadRequest, rec.Body.String())
|
|
}
|
|
})
|
|
}
|