mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-27 16:35:57 +00:00
* Add the `notify-email` column / NotifyEmail to ActionRun and set it: * services/actions/workflows.go `Dispatch` * services/actions/schedule_tasks.go `CreateScheduleTask` * services/actions/notifier_helper.go `handleWorkflows` * Only send an email if the workflow has `enable-email-notifications: true` by having `MailActionRun` return immediately if `NotifyEmail` is false. * Ignore or silently fail on `enable-email-notifications: true` parsing errors. Reporting such errors belongs in workflow validation, not when it is evaluated for the notifications. * Add unit and integration tests. Refs: https://codeberg.org/forgejo/forgejo/issues/8187 ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. ### Documentation - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] I do not want this change to show in the release notes. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8242 Reviewed-by: Christopher Besch <mail@chris-besch.com> Co-authored-by: Earl Warren <contact@earl-warren.org> Co-committed-by: Earl Warren <contact@earl-warren.org>
240 lines
9.2 KiB
Go
240 lines
9.2 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: ¬ifications,
|
|
}
|
|
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_ref", "mail.actions.run_info_previous_status", "mail.actions.run_info_trigger", "mail.view_it_on")
|
|
}
|
|
|
|
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)
|
|
})
|
|
})
|
|
}
|
|
}
|