mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
592 lines
22 KiB
Go
592 lines
22 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mailer
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"mime/quotedprintable"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
texttmpl "text/template"
|
|
|
|
actions_model "github.com/hanzoai/git/models/actions"
|
|
activities_model "github.com/hanzoai/git/models/activities"
|
|
"github.com/hanzoai/git/models/asymkey"
|
|
git_model "github.com/hanzoai/git/models/git"
|
|
"github.com/hanzoai/git/models/gituser"
|
|
issues_model "github.com/hanzoai/git/models/issues"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/models/unittest"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/git"
|
|
"github.com/hanzoai/git/modules/markup"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/storage"
|
|
"github.com/hanzoai/git/modules/templates"
|
|
"github.com/hanzoai/git/modules/test"
|
|
"github.com/hanzoai/git/services/attachment"
|
|
sender_service "github.com/hanzoai/git/services/mailer/sender"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const subjectTpl = `
|
|
{{.SubjectPrefix}}[{{.Repo}}] @{{.Doer.Name}} #{{.Issue.Index}} - {{.Issue.Title}}
|
|
`
|
|
|
|
const bodyTpl = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
|
<title>{{.Subject}}</title>
|
|
</head>
|
|
|
|
<body>
|
|
<p>{{.Body}}</p>
|
|
<p>
|
|
---
|
|
<br>
|
|
<a href="{{.Link}}">View it on the forge</a>.
|
|
</p>
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
setting.MailService = &setting.Mailer{From: "[email protected]"}
|
|
setting.Domain = "localhost"
|
|
setting.AppURL = "https://forge.example/"
|
|
|
|
doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer})
|
|
issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1, Repo: repo, Poster: doer})
|
|
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2, Issue: issue})
|
|
require.NoError(t, issue.LoadRepo(t.Context()))
|
|
return doer, repo, issue, comment
|
|
}
|
|
|
|
func prepareMailerBase64Test(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, att1, att2 *repo_model.Attachment) {
|
|
user, repo, issue, comment := prepareMailerTest(t)
|
|
setting.MailService.EmbedAttachmentImages = true
|
|
|
|
att1, err := attachment.NewAttachment(t.Context(), &repo_model.Attachment{
|
|
RepoID: repo.ID,
|
|
IssueID: issue.ID,
|
|
UploaderID: user.ID,
|
|
CommentID: comment.ID,
|
|
Name: "test.png",
|
|
}, bytes.NewReader([]byte("\x89\x50\x4e\x47\x0d\x0a\x1a\x0a")), 8)
|
|
require.NoError(t, err)
|
|
|
|
att2, err = attachment.NewAttachment(t.Context(), &repo_model.Attachment{
|
|
RepoID: repo.ID,
|
|
IssueID: issue.ID,
|
|
UploaderID: user.ID,
|
|
CommentID: comment.ID,
|
|
Name: "test.png",
|
|
}, bytes.NewReader([]byte("\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"+strings.Repeat("\x00", 1024))), 8+1024)
|
|
require.NoError(t, err)
|
|
|
|
return user, repo, issue, att1, att2
|
|
}
|
|
|
|
func mockMailTemplates(name, subjectTmpl, bodyTmpl string) func() {
|
|
return templates.MailRenderer().MockTemplate(name, subjectTmpl, bodyTmpl)
|
|
}
|
|
|
|
func TestComposeIssueComment(t *testing.T) {
|
|
doer, _, issue, comment := prepareMailerTest(t)
|
|
|
|
markup.Init(&markup.RenderHelperFuncs{
|
|
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
|
return username == doer.Name
|
|
},
|
|
})
|
|
|
|
defer test.MockVariableValue(&setting.IncomingEmail.Enabled, true)()
|
|
defer mockMailTemplates("repo/issue/comment", subjectTpl, bodyTpl)()
|
|
|
|
recipients := []*user_model.User{{Name: "Test", Email: "[email protected]"}, {Name: "Test2", Email: "[email protected]"}}
|
|
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
|
|
Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
|
|
Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index),
|
|
Comment: comment,
|
|
}, "en-US", recipients, false, "issue comment")
|
|
assert.NoError(t, err)
|
|
assert.Len(t, msgs, 2)
|
|
gomailMsg := msgs[0].ToMessage()
|
|
replyTo := gomailMsg.GetGenHeader("Reply-To")[0]
|
|
subject := gomailMsg.GetGenHeader("Subject")[0]
|
|
|
|
assert.Len(t, gomailMsg.GetAddrHeader("To"), 1, "exactly one recipient is expected in the To field")
|
|
tokenRegex := regexp.MustCompile(`\Aincoming\+(.+)@localhost\z`)
|
|
assert.Regexp(t, tokenRegex, replyTo)
|
|
token := tokenRegex.FindAllStringSubmatch(replyTo, 1)[0][1]
|
|
assert.Equal(t, "Re: ", subject[:4], "Comment reply subject should contain Re:")
|
|
assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject)
|
|
assert.Equal(t, "<user2/repo1/issues/1@localhost>", gomailMsg.GetGenHeader("In-Reply-To")[0], "In-Reply-To header doesn't match")
|
|
assert.ElementsMatch(t, []string{"<user2/repo1/issues/1@localhost>", "<reply-" + token + "@localhost>"}, gomailMsg.GetGenHeader("References"), "References header doesn't match")
|
|
assert.Equal(t, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetGenHeader("Message-ID")[0], "Message-ID header doesn't match")
|
|
assert.Equal(t, "<mailto:"+replyTo+">", gomailMsg.GetGenHeader("List-Post")[0])
|
|
assert.Len(t, gomailMsg.GetGenHeader("List-Unsubscribe"), 2) // url + mailto
|
|
|
|
var buf bytes.Buffer
|
|
_, err = gomailMsg.WriteTo(&buf)
|
|
require.NoError(t, err)
|
|
|
|
b, err := io.ReadAll(quotedprintable.NewReader(&buf))
|
|
assert.NoError(t, err)
|
|
|
|
// text/plain
|
|
assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, doer.HTMLURL(t.Context())))
|
|
assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, issue.HTMLURL(t.Context())))
|
|
|
|
// text/html
|
|
assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, doer.HTMLURL(t.Context())))
|
|
assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, issue.HTMLURL(t.Context())))
|
|
}
|
|
|
|
func TestMailMentionsComment(t *testing.T) {
|
|
doer, _, issue, comment := prepareMailerTest(t)
|
|
comment.Poster = doer
|
|
defer mockMailTemplates("repo/issue/comment", subjectTpl, bodyTpl)()
|
|
mails := 0
|
|
|
|
defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) {
|
|
mails = len(msgs)
|
|
})()
|
|
|
|
err := MailParticipantsComment(t.Context(), comment, activities_model.ActionCommentIssue, issue, []*user_model.User{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 3, mails)
|
|
}
|
|
|
|
func TestComposeIssueMessage(t *testing.T) {
|
|
doer, _, issue, _ := prepareMailerTest(t)
|
|
|
|
defer mockMailTemplates("repo/issue/new", subjectTpl, bodyTpl)()
|
|
recipients := []*user_model.User{{Name: "Test", Email: "[email protected]"}, {Name: "Test2", Email: "[email protected]"}}
|
|
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
|
|
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
|
|
Content: "test body",
|
|
}, "en-US", recipients, false, "issue create")
|
|
assert.NoError(t, err)
|
|
assert.Len(t, msgs, 2)
|
|
|
|
gomailMsg := msgs[0].ToMessage()
|
|
mailto := gomailMsg.GetAddrHeader("To")
|
|
subject := gomailMsg.GetGenHeader("Subject")
|
|
messageID := gomailMsg.GetGenHeader("Message-ID")
|
|
inReplyTo := gomailMsg.GetGenHeader("In-Reply-To")
|
|
references := gomailMsg.GetGenHeader("References")
|
|
|
|
assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field")
|
|
assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0])
|
|
assert.Equal(t, "<user2/repo1/issues/1@localhost>", inReplyTo[0], "In-Reply-To header doesn't match")
|
|
assert.Equal(t, "<user2/repo1/issues/1@localhost>", references[0], "References header doesn't match")
|
|
assert.Equal(t, "<user2/repo1/issues/1@localhost>", messageID[0], "Message-ID header doesn't match")
|
|
assert.Empty(t, gomailMsg.GetGenHeader("List-Post")) // incoming mail feature disabled
|
|
assert.Len(t, gomailMsg.GetGenHeader("List-Unsubscribe"), 1) // url without mailto
|
|
}
|
|
|
|
func TestTemplateSelection(t *testing.T) {
|
|
doer, repo, issue, comment := prepareMailerTest(t)
|
|
recipients := []*user_model.User{{Name: "Test", Email: "[email protected]"}}
|
|
|
|
defer mockMailTemplates("repo/issue/default", "repo/issue/default/subject", "repo/issue/default/body")()
|
|
defer mockMailTemplates("repo/issue/new", "repo/issue/new/subject", "repo/issue/new/body")()
|
|
defer mockMailTemplates("repo/pull/comment", "repo/pull/comment/subject", "repo/pull/comment/body")()
|
|
defer mockMailTemplates("repo/issue/close", "", "repo/issue/close/body")() // Must default to a fallback subject
|
|
|
|
expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) {
|
|
subject := msg.ToMessage().GetGenHeader("Subject")
|
|
msgbuf := new(bytes.Buffer)
|
|
_, _ = msg.ToMessage().WriteTo(msgbuf)
|
|
wholemsg := msgbuf.String()
|
|
assert.Equal(t, []string{expSubject}, subject)
|
|
assert.Contains(t, wholemsg, expBody)
|
|
}
|
|
|
|
msg := testComposeIssueCommentMessage(t, &mailComment{
|
|
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
|
|
Content: "test body",
|
|
}, recipients, false, "TestTemplateSelection")
|
|
expect(t, msg, "repo/issue/new/subject", "repo/issue/new/body")
|
|
|
|
msg = testComposeIssueCommentMessage(t, &mailComment{
|
|
Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
|
|
Content: "test body", Comment: comment,
|
|
}, recipients, false, "TestTemplateSelection")
|
|
expect(t, msg, "repo/issue/default/subject", "repo/issue/default/body")
|
|
|
|
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer})
|
|
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull})
|
|
msg = testComposeIssueCommentMessage(t, &mailComment{
|
|
Issue: pull, Doer: doer, ActionType: activities_model.ActionCommentPull,
|
|
Content: "test body", Comment: comment,
|
|
}, recipients, false, "TestTemplateSelection")
|
|
expect(t, msg, "repo/pull/comment/subject", "repo/pull/comment/body")
|
|
|
|
msg = testComposeIssueCommentMessage(t, &mailComment{
|
|
Issue: issue, Doer: doer, ActionType: activities_model.ActionCloseIssue,
|
|
Content: "test body", Comment: comment,
|
|
}, recipients, false, "TestTemplateSelection")
|
|
expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "repo/issue/close/body")
|
|
}
|
|
|
|
func TestTemplateServices(t *testing.T) {
|
|
doer, _, issue, comment := prepareMailerTest(t)
|
|
assert.NoError(t, issue.LoadRepo(t.Context()))
|
|
|
|
expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User,
|
|
actionType activities_model.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
|
|
) {
|
|
defer mockMailTemplates("repo/issue/default", tplSubject, tplBody)()
|
|
recipients := []*user_model.User{{Name: "Test", Email: "[email protected]"}}
|
|
msg := testComposeIssueCommentMessage(t, &mailComment{
|
|
Issue: issue, Doer: doer, ActionType: actionType,
|
|
Content: "test body", Comment: comment,
|
|
}, recipients, fromMention, "TestTemplateServices")
|
|
|
|
subject := msg.ToMessage().GetGenHeader("Subject")
|
|
msgbuf := new(bytes.Buffer)
|
|
_, _ = msg.ToMessage().WriteTo(msgbuf)
|
|
wholemsg := msgbuf.String()
|
|
|
|
assert.Equal(t, []string{expSubject}, subject)
|
|
assert.Contains(t, wholemsg, "\r\n"+expBody+"\r\n")
|
|
}
|
|
|
|
expect(t, issue, comment, doer, activities_model.ActionCommentIssue, false,
|
|
"{{.SubjectPrefix}}[{{.Repo}}]: @{{.Doer.Name}} commented on #{{.Issue.Index}} - {{.Issue.Title}}",
|
|
"//{{.ActionType}},{{.ActionName}},{{if .IsMention}}norender{{end}}//",
|
|
"Re: [user2/repo1]: @user2 commented on #1 - issue1",
|
|
"//issue,comment,//")
|
|
|
|
expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true,
|
|
"{{if .IsMention}}must render{{end}}",
|
|
"//subject is: {{.Subject}}//",
|
|
"must render",
|
|
"//subject is: must render//")
|
|
|
|
expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true,
|
|
"{{.FallbackSubject}}",
|
|
"//{{.SubjectPrefix}}//",
|
|
"Re: [user2/repo1] issue1 (#1)",
|
|
"//Re: //")
|
|
}
|
|
|
|
func testComposeIssueCommentMessage(t *testing.T, ctx *mailComment, recipients []*user_model.User, fromMention bool, info string) *sender_service.Message {
|
|
msgs, err := composeIssueCommentMessages(t.Context(), ctx, "en-US", recipients, fromMention, info)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, msgs, 1)
|
|
return msgs[0]
|
|
}
|
|
|
|
func TestGenerateAdditionalHeadersForIssue(t *testing.T) {
|
|
doer, _, issue, _ := prepareMailerTest(t)
|
|
|
|
comment := &mailComment{Issue: issue, Doer: doer}
|
|
recipient := &user_model.User{Name: "test", Email: "[email protected]"}
|
|
|
|
headers := generateAdditionalHeadersForIssue(t.Context(), comment, "dummy-reason", recipient)
|
|
|
|
expected := map[string]string{
|
|
"List-ID": "user2/repo1 <repo1.user2.localhost>",
|
|
"List-Archive": "<https://forge.example/user2/repo1>",
|
|
"X-Git-Reason": "dummy-reason",
|
|
"X-Git-Sender": "user2",
|
|
"X-Git-Recipient": "test",
|
|
"X-Git-Recipient-Address": "[email protected]",
|
|
"X-Git-Repository": "repo1",
|
|
"X-Git-Repository-Path": "user2/repo1",
|
|
"X-Git-Repository-Link": "https://forge.example/user2/repo1",
|
|
"X-Git-Issue-ID": "1",
|
|
"X-Git-Issue-Link": "https://forge.example/user2/repo1/issues/1",
|
|
}
|
|
|
|
for key, value := range expected {
|
|
if assert.Contains(t, headers, key) {
|
|
assert.Equal(t, value, headers[key])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateMessageIDForIssue(t *testing.T) {
|
|
_, _, issue, comment := prepareMailerTest(t)
|
|
_, _, pullIssue, _ := prepareMailerTest(t)
|
|
pullIssue.IsPull = true
|
|
|
|
type args struct {
|
|
issue *issues_model.Issue
|
|
comment *issues_model.Comment
|
|
actionType activities_model.ActionType
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
prefix string
|
|
}{
|
|
{
|
|
name: "Open Issue",
|
|
args: args{
|
|
issue: issue,
|
|
actionType: activities_model.ActionCreateIssue,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/issues/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain),
|
|
},
|
|
{
|
|
name: "Open Pull",
|
|
args: args{
|
|
issue: pullIssue,
|
|
actionType: activities_model.ActionCreatePullRequest,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/pulls/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain),
|
|
},
|
|
{
|
|
name: "Comment Issue",
|
|
args: args{
|
|
issue: issue,
|
|
comment: comment,
|
|
actionType: activities_model.ActionCommentIssue,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/issues/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
|
|
},
|
|
{
|
|
name: "Comment Pull",
|
|
args: args{
|
|
issue: pullIssue,
|
|
comment: comment,
|
|
actionType: activities_model.ActionCommentPull,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/pulls/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
|
|
},
|
|
{
|
|
name: "Close Issue",
|
|
args: args{
|
|
issue: issue,
|
|
actionType: activities_model.ActionCloseIssue,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/issues/%d/close/", issue.Repo.FullName(), issue.Index),
|
|
},
|
|
{
|
|
name: "Close Pull",
|
|
args: args{
|
|
issue: pullIssue,
|
|
actionType: activities_model.ActionClosePullRequest,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/pulls/%d/close/", issue.Repo.FullName(), issue.Index),
|
|
},
|
|
{
|
|
name: "Reopen Issue",
|
|
args: args{
|
|
issue: issue,
|
|
actionType: activities_model.ActionReopenIssue,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/issues/%d/reopen/", issue.Repo.FullName(), issue.Index),
|
|
},
|
|
{
|
|
name: "Reopen Pull",
|
|
args: args{
|
|
issue: pullIssue,
|
|
actionType: activities_model.ActionReopenPullRequest,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/pulls/%d/reopen/", issue.Repo.FullName(), issue.Index),
|
|
},
|
|
{
|
|
name: "Merge Pull",
|
|
args: args{
|
|
issue: pullIssue,
|
|
actionType: activities_model.ActionMergePullRequest,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/pulls/%d/merge/", issue.Repo.FullName(), issue.Index),
|
|
},
|
|
{
|
|
name: "Ready Pull",
|
|
args: args{
|
|
issue: pullIssue,
|
|
actionType: activities_model.ActionPullRequestReadyForReview,
|
|
},
|
|
prefix: fmt.Sprintf("<%s/pulls/%d/ready/", issue.Repo.FullName(), issue.Index),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := generateMessageIDForIssue(tt.args.issue, tt.args.comment, tt.args.actionType)
|
|
assert.True(t, strings.HasPrefix(got, tt.prefix), "%v, want %v", got, tt.prefix)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerateMessageIDForRelease(t *testing.T) {
|
|
msgID := generateMessageIDForRelease(&repo_model.Release{
|
|
ID: 1,
|
|
Repo: &repo_model.Repository{OwnerName: "owner", Name: "repo"},
|
|
})
|
|
assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID)
|
|
}
|
|
|
|
func TestGenerateMessageIDForActionsWorkflowRunStatusEmail(t *testing.T) {
|
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
|
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 795, RepoID: repo.ID})
|
|
assert.NoError(t, run.LoadAttributes(t.Context()))
|
|
msgID := generateMessageIDForActionsWorkflowRunStatusEmail(repo, run)
|
|
assert.Equal(t, "<user2/repo2/actions/runs/191@localhost>", msgID)
|
|
}
|
|
|
|
func TestFromDisplayName(t *testing.T) {
|
|
tmpl, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
|
|
assert.NoError(t, err)
|
|
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: tmpl}
|
|
defer func() { setting.MailService = nil }()
|
|
|
|
tests := []struct {
|
|
userDisplayName string
|
|
fromDisplayName string
|
|
}{{
|
|
userDisplayName: "test",
|
|
fromDisplayName: "test",
|
|
}, {
|
|
userDisplayName: "Hi Its <Mee>",
|
|
fromDisplayName: "Hi Its <Mee>",
|
|
}, {
|
|
userDisplayName: "Æsir",
|
|
fromDisplayName: "=?utf-8?q?=C3=86sir?=",
|
|
}, {
|
|
userDisplayName: "new😀user",
|
|
fromDisplayName: "=?utf-8?q?new=F0=9F=98=80user?=",
|
|
}}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.userDisplayName, func(t *testing.T) {
|
|
user := &user_model.User{FullName: tc.userDisplayName, Name: "tmp"}
|
|
got := fromDisplayName(user)
|
|
assert.Equal(t, tc.fromDisplayName, got)
|
|
})
|
|
}
|
|
|
|
t.Run("template with all available vars", func(t *testing.T) {
|
|
tmpl, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])")
|
|
assert.NoError(t, err)
|
|
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: tmpl}
|
|
oldAppName := setting.AppName
|
|
setting.AppName = "Code IT"
|
|
oldDomain := setting.Domain
|
|
setting.Domain = "code.it"
|
|
defer func() {
|
|
setting.AppName = oldAppName
|
|
setting.Domain = oldDomain
|
|
}()
|
|
|
|
assert.Equal(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
|
|
})
|
|
}
|
|
|
|
func TestEmbedBase64Images(t *testing.T) {
|
|
user, repo, issue, att1, att2 := prepareMailerBase64Test(t)
|
|
// comment := &mailComment{Issue: issue, Doer: user}
|
|
|
|
imgExternalURL := "https://via.placeholder.com/10"
|
|
imgExternalImg := fmt.Sprintf(`<img src="%s"/>`, imgExternalURL)
|
|
|
|
att1URL := setting.AppURL + repo.Owner.Name + "/" + repo.Name + "/attachments/" + att1.UUID
|
|
att1Img := fmt.Sprintf(`<img src="%s"/>`, att1URL)
|
|
att1Base64 := "data:image/png;base64,iVBORw0KGgo="
|
|
att1ImgBase64 := fmt.Sprintf(`<img src="%s"/>`, att1Base64)
|
|
|
|
att2URL := setting.AppURL + repo.Owner.Name + "/" + repo.Name + "/attachments/" + att2.UUID
|
|
att2Img := fmt.Sprintf(`<img src="%s"/>`, att2URL)
|
|
att2File, err := storage.Attachments.Open(att2.RelativePath())
|
|
require.NoError(t, err)
|
|
defer att2File.Close()
|
|
att2Bytes, err := io.ReadAll(att2File)
|
|
require.NoError(t, err)
|
|
require.Greater(t, len(att2Bytes), 1024)
|
|
att2Base64 := "data:image/png;base64," + base64.StdEncoding.EncodeToString(att2Bytes)
|
|
att2ImgBase64 := fmt.Sprintf(`<img src="%s"/>`, att2Base64)
|
|
|
|
t.Run("ComposeMessage", func(t *testing.T) {
|
|
defer mockMailTemplates("repo/issue/new", subjectTpl, bodyTpl)()
|
|
|
|
issue.Content = fmt.Sprintf(`MSG-BEFORE <image src="attachments/%s"> MSG-AFTER`, att1.UUID)
|
|
require.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue, "content"))
|
|
|
|
recipients := []*user_model.User{{Name: "Test", Email: "[email protected]"}}
|
|
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
|
|
Issue: issue,
|
|
Doer: user,
|
|
ActionType: activities_model.ActionCreateIssue,
|
|
Content: issue.Content,
|
|
}, "en-US", recipients, false, "issue create")
|
|
require.NoError(t, err)
|
|
|
|
mailBody := msgs[0].Body
|
|
assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo=".*/></a> MSG-AFTER`, mailBody)
|
|
})
|
|
|
|
t.Run("EmbedInstanceImageSkipExternalImage", func(t *testing.T) {
|
|
mailBody := "<html><head></head><body><p>Test1</p>" + imgExternalImg + "<p>Test2</p>" + att1Img + "<p>Test3</p></body></html>"
|
|
expectedMailBody := "<html><head></head><body><p>Test1</p>" + imgExternalImg + "<p>Test2</p>" + att1ImgBase64 + "<p>Test3</p></body></html>"
|
|
b64embedder := newMailAttachmentBase64Embedder(user, repo, 1024)
|
|
resultMailBody, err := b64embedder.Base64InlineImages(t.Context(), template.HTML(mailBody))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedMailBody, string(resultMailBody))
|
|
})
|
|
|
|
t.Run("LimitedEmailBodySize", func(t *testing.T) {
|
|
mailBody := fmt.Sprintf("<html><head></head><body>%s%s</body></html>", att1Img, att2Img)
|
|
b64embedder := newMailAttachmentBase64Embedder(user, repo, 1024)
|
|
resultMailBody, err := b64embedder.Base64InlineImages(t.Context(), template.HTML(mailBody))
|
|
require.NoError(t, err)
|
|
expected := fmt.Sprintf("<html><head></head><body>%s%s</body></html>", att1ImgBase64, att2Img)
|
|
assert.Equal(t, expected, string(resultMailBody))
|
|
|
|
b64embedder = newMailAttachmentBase64Embedder(user, repo, 4096)
|
|
resultMailBody, err = b64embedder.Base64InlineImages(t.Context(), template.HTML(mailBody))
|
|
require.NoError(t, err)
|
|
expected = fmt.Sprintf("<html><head></head><body>%s%s</body></html>", att1ImgBase64, att2ImgBase64)
|
|
assert.Equal(t, expected, string(resultMailBody))
|
|
})
|
|
}
|
|
|
|
func TestMailPullRequestPush(t *testing.T) {
|
|
doer, _, issue, comment := prepareMailerTest(t)
|
|
mc := &mailComment{
|
|
Issue: issue,
|
|
Comment: comment,
|
|
Doer: doer,
|
|
}
|
|
issue.IsPull = true
|
|
issue.PullRequest = &issues_model.PullRequest{BaseRepo: mc.Issue.Repo}
|
|
mc.Comment.Type = issues_model.CommentTypePullRequestPush
|
|
mc.Comment.Commits = []*git_model.SignCommitWithStatuses{
|
|
{
|
|
SignCommit: &asymkey.SignCommit{
|
|
UserCommit: &gituser.UserCommit{
|
|
GitCommit: &git.Commit{
|
|
CommitMessage: git.CommitMessage{MessageRaw: "test commit msg"},
|
|
ID: git.Sha1ObjectFormat.EmptyObjectID(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
msgs, err := composeIssueCommentMessages(t.Context(), mc, "mock", []*user_model.User{{Name: "Test", Email: "[email protected]"}}, false, "pull request push")
|
|
require.NoError(t, err)
|
|
assert.Contains(t, msgs[0].Body, `<a href="https://forge.example/user2/repo1/commit/0000000000000000000000000000000000000000">0000000000</a> - test commit msg`)
|
|
assert.Contains(t, msgs[0].Body, `</html>`)
|
|
}
|