mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 22:41:03 +00:00 
			
		
		
		
	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. ## 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... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8448 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>
		
			
				
	
	
		
			325 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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: ¬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_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)
 | |
| 			})
 | |
| 		})
 | |
| 	}
 | |
| }
 |