code.gitea.io/gitea@v1.22.3/services/mailer/mail_test.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package mailer 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "html/template" 11 "io" 12 "mime/quotedprintable" 13 "regexp" 14 "strings" 15 "testing" 16 texttmpl "text/template" 17 18 activities_model "code.gitea.io/gitea/models/activities" 19 "code.gitea.io/gitea/models/db" 20 issues_model "code.gitea.io/gitea/models/issues" 21 repo_model "code.gitea.io/gitea/models/repo" 22 "code.gitea.io/gitea/models/unittest" 23 user_model "code.gitea.io/gitea/models/user" 24 "code.gitea.io/gitea/modules/markup" 25 "code.gitea.io/gitea/modules/setting" 26 27 "github.com/stretchr/testify/assert" 28 ) 29 30 const subjectTpl = ` 31 {{.SubjectPrefix}}[{{.Repo}}] @{{.Doer.Name}} #{{.Issue.Index}} - {{.Issue.Title}} 32 ` 33 34 const bodyTpl = ` 35 <!DOCTYPE html> 36 <html> 37 <head> 38 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 39 <title>{{.Subject}}</title> 40 </head> 41 42 <body> 43 <p>{{.Body}}</p> 44 <p> 45 --- 46 <br> 47 <a href="{{.Link}}">View it on Gitea</a>. 48 </p> 49 </body> 50 </html> 51 ` 52 53 func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) { 54 assert.NoError(t, unittest.PrepareTestDatabase()) 55 mailService := setting.Mailer{ 56 From: "test@gitea.com", 57 } 58 59 setting.MailService = &mailService 60 setting.Domain = "localhost" 61 62 doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 63 repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer}) 64 issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1, Repo: repo, Poster: doer}) 65 assert.NoError(t, issue.LoadRepo(db.DefaultContext)) 66 comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2, Issue: issue}) 67 return doer, repo, issue, comment 68 } 69 70 func TestComposeIssueCommentMessage(t *testing.T) { 71 doer, _, issue, comment := prepareMailerTest(t) 72 73 markup.Init(&markup.ProcessorHelper{ 74 IsUsernameMentionable: func(ctx context.Context, username string) bool { 75 return username == doer.Name 76 }, 77 }) 78 79 setting.IncomingEmail.Enabled = true 80 defer func() { setting.IncomingEmail.Enabled = false }() 81 82 subjectTemplates = texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl)) 83 bodyTemplates = template.Must(template.New("issue/comment").Parse(bodyTpl)) 84 85 recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}} 86 msgs, err := composeIssueCommentMessages(&mailCommentContext{ 87 Context: context.TODO(), // TODO: use a correct context 88 Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue, 89 Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index), 90 Comment: comment, 91 }, "en-US", recipients, false, "issue comment") 92 assert.NoError(t, err) 93 assert.Len(t, msgs, 2) 94 gomailMsg := msgs[0].ToMessage() 95 replyTo := gomailMsg.GetHeader("Reply-To")[0] 96 subject := gomailMsg.GetHeader("Subject")[0] 97 98 assert.Len(t, gomailMsg.GetHeader("To"), 1, "exactly one recipient is expected in the To field") 99 tokenRegex := regexp.MustCompile(`\Aincoming\+(.+)@localhost\z`) 100 assert.Regexp(t, tokenRegex, replyTo) 101 token := tokenRegex.FindAllStringSubmatch(replyTo, 1)[0][1] 102 assert.Equal(t, "Re: ", subject[:4], "Comment reply subject should contain Re:") 103 assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject) 104 assert.Equal(t, "<user2/repo1/issues/1@localhost>", gomailMsg.GetHeader("In-Reply-To")[0], "In-Reply-To header doesn't match") 105 assert.ElementsMatch(t, []string{"<user2/repo1/issues/1@localhost>", "<reply-" + token + "@localhost>"}, gomailMsg.GetHeader("References"), "References header doesn't match") 106 assert.Equal(t, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match") 107 assert.Equal(t, "<mailto:"+replyTo+">", gomailMsg.GetHeader("List-Post")[0]) 108 assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto 109 110 var buf bytes.Buffer 111 gomailMsg.WriteTo(&buf) 112 113 b, err := io.ReadAll(quotedprintable.NewReader(&buf)) 114 assert.NoError(t, err) 115 116 // text/plain 117 assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, doer.HTMLURL())) 118 assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, issue.HTMLURL())) 119 120 // text/html 121 assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, doer.HTMLURL())) 122 assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, issue.HTMLURL())) 123 } 124 125 func TestComposeIssueMessage(t *testing.T) { 126 doer, _, issue, _ := prepareMailerTest(t) 127 128 subjectTemplates = texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl)) 129 bodyTemplates = template.Must(template.New("issue/new").Parse(bodyTpl)) 130 131 recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}} 132 msgs, err := composeIssueCommentMessages(&mailCommentContext{ 133 Context: context.TODO(), // TODO: use a correct context 134 Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue, 135 Content: "test body", 136 }, "en-US", recipients, false, "issue create") 137 assert.NoError(t, err) 138 assert.Len(t, msgs, 2) 139 140 gomailMsg := msgs[0].ToMessage() 141 mailto := gomailMsg.GetHeader("To") 142 subject := gomailMsg.GetHeader("Subject") 143 messageID := gomailMsg.GetHeader("Message-ID") 144 inReplyTo := gomailMsg.GetHeader("In-Reply-To") 145 references := gomailMsg.GetHeader("References") 146 147 assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field") 148 assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0]) 149 assert.Equal(t, "<user2/repo1/issues/1@localhost>", inReplyTo[0], "In-Reply-To header doesn't match") 150 assert.Equal(t, "<user2/repo1/issues/1@localhost>", references[0], "References header doesn't match") 151 assert.Equal(t, "<user2/repo1/issues/1@localhost>", messageID[0], "Message-ID header doesn't match") 152 assert.Empty(t, gomailMsg.GetHeader("List-Post")) // incoming mail feature disabled 153 assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 1) // url without mailto 154 } 155 156 func TestTemplateSelection(t *testing.T) { 157 doer, repo, issue, comment := prepareMailerTest(t) 158 recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}} 159 160 subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject")) 161 texttmpl.Must(subjectTemplates.New("issue/new").Parse("issue/new/subject")) 162 texttmpl.Must(subjectTemplates.New("pull/comment").Parse("pull/comment/subject")) 163 texttmpl.Must(subjectTemplates.New("issue/close").Parse("")) // Must default to fallback subject 164 165 bodyTemplates = template.Must(template.New("issue/default").Parse("issue/default/body")) 166 template.Must(bodyTemplates.New("issue/new").Parse("issue/new/body")) 167 template.Must(bodyTemplates.New("pull/comment").Parse("pull/comment/body")) 168 template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body")) 169 170 expect := func(t *testing.T, msg *Message, expSubject, expBody string) { 171 subject := msg.ToMessage().GetHeader("Subject") 172 msgbuf := new(bytes.Buffer) 173 _, _ = msg.ToMessage().WriteTo(msgbuf) 174 wholemsg := msgbuf.String() 175 assert.Equal(t, []string{expSubject}, subject) 176 assert.Contains(t, wholemsg, expBody) 177 } 178 179 msg := testComposeIssueCommentMessage(t, &mailCommentContext{ 180 Context: context.TODO(), // TODO: use a correct context 181 Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue, 182 Content: "test body", 183 }, recipients, false, "TestTemplateSelection") 184 expect(t, msg, "issue/new/subject", "issue/new/body") 185 186 msg = testComposeIssueCommentMessage(t, &mailCommentContext{ 187 Context: context.TODO(), // TODO: use a correct context 188 Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue, 189 Content: "test body", Comment: comment, 190 }, recipients, false, "TestTemplateSelection") 191 expect(t, msg, "issue/default/subject", "issue/default/body") 192 193 pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer}) 194 comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull}) 195 msg = testComposeIssueCommentMessage(t, &mailCommentContext{ 196 Context: context.TODO(), // TODO: use a correct context 197 Issue: pull, Doer: doer, ActionType: activities_model.ActionCommentPull, 198 Content: "test body", Comment: comment, 199 }, recipients, false, "TestTemplateSelection") 200 expect(t, msg, "pull/comment/subject", "pull/comment/body") 201 202 msg = testComposeIssueCommentMessage(t, &mailCommentContext{ 203 Context: context.TODO(), // TODO: use a correct context 204 Issue: issue, Doer: doer, ActionType: activities_model.ActionCloseIssue, 205 Content: "test body", Comment: comment, 206 }, recipients, false, "TestTemplateSelection") 207 expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "issue/close/body") 208 } 209 210 func TestTemplateServices(t *testing.T) { 211 doer, _, issue, comment := prepareMailerTest(t) 212 assert.NoError(t, issue.LoadRepo(db.DefaultContext)) 213 214 expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User, 215 actionType activities_model.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string, 216 ) { 217 subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject)) 218 bodyTemplates = template.Must(template.New("issue/default").Parse(tplBody)) 219 220 recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}} 221 msg := testComposeIssueCommentMessage(t, &mailCommentContext{ 222 Context: context.TODO(), // TODO: use a correct context 223 Issue: issue, Doer: doer, ActionType: actionType, 224 Content: "test body", Comment: comment, 225 }, recipients, fromMention, "TestTemplateServices") 226 227 subject := msg.ToMessage().GetHeader("Subject") 228 msgbuf := new(bytes.Buffer) 229 _, _ = msg.ToMessage().WriteTo(msgbuf) 230 wholemsg := msgbuf.String() 231 232 assert.Equal(t, []string{expSubject}, subject) 233 assert.Contains(t, wholemsg, "\r\n"+expBody+"\r\n") 234 } 235 236 expect(t, issue, comment, doer, activities_model.ActionCommentIssue, false, 237 "{{.SubjectPrefix}}[{{.Repo}}]: @{{.Doer.Name}} commented on #{{.Issue.Index}} - {{.Issue.Title}}", 238 "//{{.ActionType}},{{.ActionName}},{{if .IsMention}}norender{{end}}//", 239 "Re: [user2/repo1]: @user2 commented on #1 - issue1", 240 "//issue,comment,//") 241 242 expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true, 243 "{{if .IsMention}}must render{{end}}", 244 "//subject is: {{.Subject}}//", 245 "must render", 246 "//subject is: must render//") 247 248 expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true, 249 "{{.FallbackSubject}}", 250 "//{{.SubjectPrefix}}//", 251 "Re: [user2/repo1] issue1 (#1)", 252 "//Re: //") 253 } 254 255 func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recipients []*user_model.User, fromMention bool, info string) *Message { 256 msgs, err := composeIssueCommentMessages(ctx, "en-US", recipients, fromMention, info) 257 assert.NoError(t, err) 258 assert.Len(t, msgs, 1) 259 return msgs[0] 260 } 261 262 func TestGenerateAdditionalHeaders(t *testing.T) { 263 doer, _, issue, _ := prepareMailerTest(t) 264 265 ctx := &mailCommentContext{Context: context.TODO() /* TODO: use a correct context */, Issue: issue, Doer: doer} 266 recipient := &user_model.User{Name: "test", Email: "test@gitea.com"} 267 268 headers := generateAdditionalHeaders(ctx, "dummy-reason", recipient) 269 270 expected := map[string]string{ 271 "List-ID": "user2/repo1 <repo1.user2.localhost>", 272 "List-Archive": "<https://try.gitea.io/user2/repo1>", 273 "X-Gitea-Reason": "dummy-reason", 274 "X-Gitea-Sender": "user2", 275 "X-Gitea-Recipient": "test", 276 "X-Gitea-Recipient-Address": "test@gitea.com", 277 "X-Gitea-Repository": "repo1", 278 "X-Gitea-Repository-Path": "user2/repo1", 279 "X-Gitea-Repository-Link": "https://try.gitea.io/user2/repo1", 280 "X-Gitea-Issue-ID": "1", 281 "X-Gitea-Issue-Link": "https://try.gitea.io/user2/repo1/issues/1", 282 } 283 284 for key, value := range expected { 285 if assert.Contains(t, headers, key) { 286 assert.Equal(t, value, headers[key]) 287 } 288 } 289 } 290 291 func TestGenerateMessageIDForIssue(t *testing.T) { 292 _, _, issue, comment := prepareMailerTest(t) 293 _, _, pullIssue, _ := prepareMailerTest(t) 294 pullIssue.IsPull = true 295 296 type args struct { 297 issue *issues_model.Issue 298 comment *issues_model.Comment 299 actionType activities_model.ActionType 300 } 301 tests := []struct { 302 name string 303 args args 304 prefix string 305 }{ 306 { 307 name: "Open Issue", 308 args: args{ 309 issue: issue, 310 actionType: activities_model.ActionCreateIssue, 311 }, 312 prefix: fmt.Sprintf("<%s/issues/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain), 313 }, 314 { 315 name: "Open Pull", 316 args: args{ 317 issue: pullIssue, 318 actionType: activities_model.ActionCreatePullRequest, 319 }, 320 prefix: fmt.Sprintf("<%s/pulls/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain), 321 }, 322 { 323 name: "Comment Issue", 324 args: args{ 325 issue: issue, 326 comment: comment, 327 actionType: activities_model.ActionCommentIssue, 328 }, 329 prefix: fmt.Sprintf("<%s/issues/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain), 330 }, 331 { 332 name: "Comment Pull", 333 args: args{ 334 issue: pullIssue, 335 comment: comment, 336 actionType: activities_model.ActionCommentPull, 337 }, 338 prefix: fmt.Sprintf("<%s/pulls/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain), 339 }, 340 { 341 name: "Close Issue", 342 args: args{ 343 issue: issue, 344 actionType: activities_model.ActionCloseIssue, 345 }, 346 prefix: fmt.Sprintf("<%s/issues/%d/close/", issue.Repo.FullName(), issue.Index), 347 }, 348 { 349 name: "Close Pull", 350 args: args{ 351 issue: pullIssue, 352 actionType: activities_model.ActionClosePullRequest, 353 }, 354 prefix: fmt.Sprintf("<%s/pulls/%d/close/", issue.Repo.FullName(), issue.Index), 355 }, 356 { 357 name: "Reopen Issue", 358 args: args{ 359 issue: issue, 360 actionType: activities_model.ActionReopenIssue, 361 }, 362 prefix: fmt.Sprintf("<%s/issues/%d/reopen/", issue.Repo.FullName(), issue.Index), 363 }, 364 { 365 name: "Reopen Pull", 366 args: args{ 367 issue: pullIssue, 368 actionType: activities_model.ActionReopenPullRequest, 369 }, 370 prefix: fmt.Sprintf("<%s/pulls/%d/reopen/", issue.Repo.FullName(), issue.Index), 371 }, 372 { 373 name: "Merge Pull", 374 args: args{ 375 issue: pullIssue, 376 actionType: activities_model.ActionMergePullRequest, 377 }, 378 prefix: fmt.Sprintf("<%s/pulls/%d/merge/", issue.Repo.FullName(), issue.Index), 379 }, 380 { 381 name: "Ready Pull", 382 args: args{ 383 issue: pullIssue, 384 actionType: activities_model.ActionPullRequestReadyForReview, 385 }, 386 prefix: fmt.Sprintf("<%s/pulls/%d/ready/", issue.Repo.FullName(), issue.Index), 387 }, 388 } 389 for _, tt := range tests { 390 t.Run(tt.name, func(t *testing.T) { 391 got := generateMessageIDForIssue(tt.args.issue, tt.args.comment, tt.args.actionType) 392 if !strings.HasPrefix(got, tt.prefix) { 393 t.Errorf("generateMessageIDForIssue() = %v, want %v", got, tt.prefix) 394 } 395 }) 396 } 397 } 398 399 func TestGenerateMessageIDForRelease(t *testing.T) { 400 msgID := generateMessageIDForRelease(&repo_model.Release{ 401 ID: 1, 402 Repo: &repo_model.Repository{OwnerName: "owner", Name: "repo"}, 403 }) 404 assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID) 405 }