1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo.git synced 2025-06-27 16:35:57 +00:00
forgejo/services/mailer/mail_actions_now_done_test.go
Earl Warren b2c4fc9f94 bug: Forgejo Actions email notifications are opt-in (#8242)
* 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>
2025-06-21 13:11:01 +02:00

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: &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_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)
})
})
}
}