// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: GPL-3.0-or-later package mailer import ( "testing" actions_model "forgejo.org/models/actions" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" notify_service "forgejo.org/services/notify" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func getActionsNowDoneTestUsers(t *testing.T) []*user_model.User { t.Helper() newTriggerUser := new(user_model.User) newTriggerUser.Name = "new_trigger_user" newTriggerUser.Language = "en_US" newTriggerUser.IsAdmin = false newTriggerUser.Email = "new_trigger_user@example.com" newTriggerUser.LastLoginUnix = 1693648327 newTriggerUser.CreatedUnix = 1693648027 newTriggerUser.EmailNotificationsPreference = user_model.EmailNotificationsEnabled require.NoError(t, user_model.CreateUser(db.DefaultContext, newTriggerUser)) newOwner := new(user_model.User) newOwner.Name = "new_owner" newOwner.Language = "en_US" newOwner.IsAdmin = false newOwner.Email = "new_owner@example.com" newOwner.LastLoginUnix = 1693648329 newOwner.CreatedUnix = 1693648029 newOwner.EmailNotificationsPreference = user_model.EmailNotificationsEnabled require.NoError(t, user_model.CreateUser(db.DefaultContext, newOwner)) return []*user_model.User{newTriggerUser, newOwner} } 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() users := getActionsNowDoneTestUsers(t) defer CleanUpUsers(ctx, users) triggerUser := users[0] ownerUser := users[1] repo := repo_model.Repository{ Name: "some repo", Description: "rockets are cool", Owner: ownerUser, OwnerID: ownerUser.ID, } // 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", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusFailure, Stopped: 1745821796, TriggerEvent: "workflow_dispatch"} run2 := &actions_model.ActionRun{ID: 1, Repo: &repo, RepoID: repo.ID, Title: "some workflow", TriggerUser: triggerUser, TriggerUserID: triggerUser.ID, Status: actions_model.StatusSuccess, Stopped: 1745822796, TriggerEvent: "push"} notify_service.RegisterNotifier(NewNotifier()) t.Run("DontSendNotificationEmailOnFirstActionSuccess", func(t *testing.T) { defer MockMailSettings(func(msgs ...*Message) { assert.Fail(t, "no mail should be sent") })() notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, nil) }) t.Run("SendNotificationEmailOnActionRunFailed", func(t *testing.T) { mailSentToOwner := false mailSentToTriggerUser := false defer MockMailSettings(func(msgs ...*Message) { assert.LessOrEqual(t, len(msgs), 2) for _, msg := range msgs { switch msg.To { case triggerUser.EmailTo(): assert.False(t, mailSentToTriggerUser, "sent mail twice") mailSentToTriggerUser = true case ownerUser.EmailTo(): assert.False(t, mailSentToOwner, "sent mail twice") mailSentToOwner = true default: assert.Fail(t, "sent mail to unknown sender", msg.To) } assert.Contains(t, msg.Body, triggerUser.HTMLURL()) assert.Contains(t, msg.Body, 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) } })() notify_service.ActionRunNowDone(ctx, run1, actions_model.StatusWaiting, nil) assert.True(t, mailSentToOwner) assert.True(t, mailSentToTriggerUser) }) t.Run("SendNotificationEmailOnActionRunRecovered", func(t *testing.T) { mailSentToOwner := false mailSentToTriggerUser := false defer MockMailSettings(func(msgs ...*Message) { assert.LessOrEqual(t, len(msgs), 2) for _, msg := range msgs { switch msg.To { case triggerUser.EmailTo(): assert.False(t, mailSentToTriggerUser, "sent mail twice") mailSentToTriggerUser = true case ownerUser.EmailTo(): assert.False(t, mailSentToOwner, "sent mail twice") mailSentToOwner = true default: assert.Fail(t, "sent mail to unknown sender", msg.To) } assert.Contains(t, msg.Body, triggerUser.HTMLURL()) assert.Contains(t, msg.Body, 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") assertTranslatedLocaleMailActionsNowDone(t, msg.Body) } })() assert.NotNil(t, setting.MailService) notify_service.ActionRunNowDone(ctx, run2, actions_model.StatusRunning, run1) assert.True(t, mailSentToOwner) assert.True(t, mailSentToTriggerUser) }) }