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