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  }