forgejo/services/mailer/mail_actions_now_done_test.go
forgejo-backport-action eb543dcbdb [v12.0/forgejo] fix(email): actions notification template confuses branch with PR (#8455)
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/8448

When a mail notification is sent because of a failed pull request run,
the body will show:

Branch: #661 (f57df45)

where #661 is the number of the pull request and not the branch. This
is because run.PrettyRef() is used and has a misleading special case
returning a PR number instead of a ref.

Remove the textual description as it can easily be discovered from the
run web page linked in the mail.

Co-authored-by: Earl Warren <contact@earl-warren.org>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8455
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
2025-07-09 10:20:54 +02:00

325 lines
11 KiB
Go

// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package mailer
import (
"slices"
"testing"
actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
organization_model "forgejo.org/models/organization"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
"forgejo.org/modules/optional"
"forgejo.org/modules/setting"
"forgejo.org/modules/test"
notify_service "forgejo.org/services/notify"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func getActionsNowDoneTestUser(t *testing.T, name, email, notifications string) *user_model.User {
t.Helper()
user := new(user_model.User)
user.Name = name
user.Language = "en_US"
user.IsAdmin = false
user.Email = email
user.LastLoginUnix = 1693648327
user.CreatedUnix = 1693648027
opts := user_model.CreateUserOverwriteOptions{
AllowCreateOrganization: optional.Some(true),
EmailNotificationsPreference: &notifications,
}
require.NoError(t, user_model.AdminCreateUser(db.DefaultContext, user, &opts))
return user
}
func getActionsNowDoneTestOrg(t *testing.T, name, email string, owner *user_model.User) *user_model.User {
t.Helper()
org := new(organization_model.Organization)
org.Name = name
org.Language = "en_US"
org.IsAdmin = false
// contact email for the organization, for display purposes but otherwise not used as of v12
org.Email = email
org.LastLoginUnix = 1693648327
org.CreatedUnix = 1693648027
org.Email = email
require.NoError(t, organization_model.CreateOrganization(db.DefaultContext, org, owner))
return (*user_model.User)(org)
}
func assertTranslatedLocaleMailActionsNowDone(t *testing.T, msgBody string) {
AssertTranslatedLocale(t, msgBody, "mail.actions.successful_run_after_failure", "mail.actions.not_successful_run", "mail.actions.run_info_cur_status", "mail.actions.run_info_sha", "mail.actions.run_info_previous_status", "mail.actions.run_info_trigger", "mail.view_it_on")
}
func TestActionRunNowDoneStatusMatrix(t *testing.T) {
successStatuses := []actions_model.Status{
actions_model.StatusSuccess,
actions_model.StatusSkipped,
actions_model.StatusCancelled,
}
failureStatuses := []actions_model.Status{
actions_model.StatusFailure,
}
for _, testCase := range []struct {
name string
statuses []actions_model.Status
hasLastRun bool
lastStatuses []actions_model.Status
run bool
}{
{
name: "FailureNoLastRun",
statuses: failureStatuses,
run: true,
},
{
name: "SuccessNoLastRun",
statuses: successStatuses,
run: false,
},
{
name: "FailureLastRunSuccess",
statuses: failureStatuses,
hasLastRun: true,
lastStatuses: successStatuses,
run: true,
},
{
name: "FailureLastRunFailure",
statuses: failureStatuses,
hasLastRun: true,
lastStatuses: failureStatuses,
run: true,
},
{
name: "SuccessLastRunFailure",
statuses: successStatuses,
hasLastRun: true,
lastStatuses: failureStatuses,
run: true,
},
{
name: "SuccessLastRunSuccess",
statuses: successStatuses,
hasLastRun: true,
lastStatuses: successStatuses,
run: false,
},
} {
t.Run(testCase.name, func(t *testing.T) {
var called bool
defer test.MockVariableValue(&MailActionRun, func(run *actions_model.ActionRun, priorStatus actions_model.Status, lastRun *actions_model.ActionRun) error {
called = true
return nil
})()
for _, status := range testCase.statuses {
for _, lastStatus := range testCase.lastStatuses {
called = false
n := NewNotifier()
var lastRun *actions_model.ActionRun
if testCase.hasLastRun {
lastRun = &actions_model.ActionRun{
Status: lastStatus,
}
}
n.ActionRunNowDone(t.Context(),
&actions_model.ActionRun{
Status: status,
},
actions_model.StatusUnknown,
lastRun)
assert.Equal(t, testCase.run, called, "status = %s, lastStatus = %s", status, lastStatus)
}
}
})
}
}
func TestActionRunNowDoneNotificationMail(t *testing.T) {
ctx := t.Context()
defer test.MockVariableValue(&setting.Admin.DisableRegularOrgCreation, false)()
actionsUser := user_model.NewActionsUser()
require.NotEmpty(t, actionsUser.Email)
repo := repo_model.Repository{
Name: "some repo",
Description: "rockets are cool",
}
// Do some funky stuff with the action run's ids:
// The run with the larger ID finished first.
// This is odd but something that must work.
run1 := &actions_model.ActionRun{ID: 2, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"}
run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"}
assignUsers := func(triggerUser, owner *user_model.User) {
for _, run := range []*actions_model.ActionRun{run1, run2} {
run.TriggerUser = triggerUser
run.TriggerUserID = triggerUser.ID
run.NotifyEmail = true
}
repo.Owner = owner
repo.OwnerID = owner.ID
}
notify_service.RegisterNotifier(NewNotifier())
orgOwner := getActionsNowDoneTestUser(t, "org_owner", "org_owner@example.com", "disabled")
defer CleanUpUsers(ctx, []*user_model.User{orgOwner})
t.Run("DontSendNotificationEmailOnFirstActionSuccess", func(t *testing.T) {
user := getActionsNowDoneTestUser(t, "new_user", "new_user@example.com", "enabled")
defer CleanUpUsers(ctx, []*user_model.User{user})
assignUsers(user, user)
defer MockMailSettings(func(msgs ...*Message) {
assert.Fail(t, "no mail should be sent")
})()
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
})
t.Run("WorkflowEnableEmailNotificationIsFalse", func(t *testing.T) {
user := getActionsNowDoneTestUser(t, "new_user1", "new_user1@example.com", "enabled")
defer CleanUpUsers(ctx, []*user_model.User{user})
assignUsers(user, user)
defer MockMailSettings(func(msgs ...*Message) {
assert.Fail(t, "no mail should be sent")
})()
run2.NotifyEmail = false
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil)
})
for _, testCase := range []struct {
name string
triggerUser *user_model.User
owner *user_model.User
expected string
expectMail bool
}{
{
// if the action is assigned a trigger user in a repository
// owned by a regular user, the mail is sent to the trigger user
name: "RegularTriggerUser",
triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user0", "new_trigger_user0@example.com", user_model.EmailNotificationsEnabled),
owner: getActionsNowDoneTestUser(t, "new_owner0", "new_owner0@example.com", user_model.EmailNotificationsEnabled),
expected: "trigger",
expectMail: true,
},
{
// if the action is assigned to a system user (e.g. ActionsUser)
// in a repository owned by a regular user, the mail is sent to
// the user that owns the repository
name: "SystemTriggerUserAndRegularOwner",
triggerUser: actionsUser,
owner: getActionsNowDoneTestUser(t, "new_owner1", "new_owner1@example.com", user_model.EmailNotificationsEnabled),
expected: "owner",
expectMail: true,
},
{
// if the action is assigned a trigger user with disabled notifications in a repository
// owned by a regular user, no mail is sent
name: "RegularTriggerUserNotificationsDisabled",
triggerUser: getActionsNowDoneTestUser(t, "new_trigger_user2", "new_trigger_user2@example.com", user_model.EmailNotificationsDisabled),
owner: getActionsNowDoneTestUser(t, "new_owner2", "new_owner2@example.com", user_model.EmailNotificationsEnabled),
expectMail: false,
},
{
// if the action is assigned to a system user (e.g. ActionsUser)
// owned by a regular user with disabled notifications, no mail is sent
name: "SystemTriggerUserAndRegularOwnerNotificationsDisabled",
triggerUser: actionsUser,
owner: getActionsNowDoneTestUser(t, "new_owner3", "new_owner3@example.com", user_model.EmailNotificationsDisabled),
expectMail: false,
},
{
// if the action is assigned to a system user (e.g. ActionsUser)
// in a repository owned by an organization with an email contact, the mail is sent to
// this email contact
name: "SystemTriggerUserAndOrgOwner",
triggerUser: actionsUser,
owner: getActionsNowDoneTestOrg(t, "new_org1", "new_org_owner0@example.com", orgOwner),
expected: "owner",
expectMail: true,
},
{
// if the action is assigned to a system user (e.g. ActionsUser)
// in a repository owned by an organization without an email contact, no mail is sent
name: "SystemTriggerUserAndNoMailOrgOwner",
triggerUser: actionsUser,
owner: getActionsNowDoneTestOrg(t, "new_org2", "", orgOwner),
expectMail: false,
},
} {
t.Run(testCase.name, func(t *testing.T) {
assignUsers(testCase.triggerUser, testCase.owner)
defer CleanUpUsers(ctx, slices.DeleteFunc([]*user_model.User{testCase.triggerUser, testCase.owner}, func(user *user_model.User) bool {
return user.IsSystem()
}))
t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) {
mailSent := false
defer MockMailSettings(func(msgs ...*Message) {
assert.Len(t, msgs, 1)
msg := msgs[0]
assert.False(t, mailSent, "sent mail twice")
expectedEmail := testCase.triggerUser.Email
if testCase.expected == "owner" { // otherwise "trigger"
expectedEmail = testCase.owner.Email
}
require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender")
mailSent = true
assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL())
assert.Contains(t, msg.Body, testCase.triggerUser.Name)
// what happened
assert.Contains(t, msg.Body, "failed")
// new status of run
assert.Contains(t, msg.Body, "failure")
// prior status of this run
assert.Contains(t, msg.Body, "waiting")
assertTranslatedLocaleMailActionsNowDone(t, msg.Body)
})()
require.NotNil(t, setting.MailService)
notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil)
assert.Equal(t, testCase.expectMail, mailSent)
})
t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) {
mailSent := false
defer MockMailSettings(func(msgs ...*Message) {
assert.Len(t, msgs, 1)
msg := msgs[0]
assert.False(t, mailSent, "sent mail twice")
expectedEmail := testCase.triggerUser.Email
if testCase.expected == "owner" { // otherwise "trigger"
expectedEmail = testCase.owner.Email
}
require.Contains(t, msg.To, expectedEmail, "sent mail to unknown sender")
mailSent = true
assert.Contains(t, msg.Body, testCase.triggerUser.HTMLURL())
assert.Contains(t, msg.Body, testCase.triggerUser.Name)
// what happened
assert.Contains(t, msg.Body, "recovered")
// old status of run
assert.Contains(t, msg.Body, "failure")
// new status of run
assert.Contains(t, msg.Body, "success")
// prior status of this run
assert.Contains(t, msg.Body, "running")
})()
require.NotNil(t, setting.MailService)
notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1)
assert.Equal(t, testCase.expectMail, mailSent)
})
})
}
}