mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
test(coderd/notifications): share one pipeline across golden templates (#29459)
`TestNotificationTemplates_Golden` built a coderd server, a second database and a mock SMTP server for each of its 86 template and method cases. The rendered output only depends on the recipient row and the appearance settings, so the test now builds one database, one recipient, one manager and one mock SMTP and webhook server for the whole run, and gives an isolated pipeline only to the two templates that render a different recipient or appearance (`TemplateUserRequestedOneTimePasscode`, `TemplateWorkspaceDeleted_CustomAppearance`). Deliveries are serialized per method because the mock SMTP backend records a single message. Test names, the `-update` flag and `make gen/golden-files` are unchanged, and the golden files are byte-identical. ## Before / After Local, `go test -count=1 -tags=testsmallbatch -parallel=8`, Postgres on localhost: | Measurement | Before | After | |---|---|---| | `TestNotificationTemplates_Golden` (`ok` line for `-run`) | 5.09s | 1.33s | | `coderd/notifications` package | 6.55s | 2.84s | | Sum of the 86 leaf subtest durations (slot time) | 38.4s | 8.3s | ## Validation - `go test -count=1 -tags=testsmallbatch -parallel=8 ./coderd/notifications/ -run '^TestNotificationTemplates_Golden$'` and the whole package pass - `TZ=UTC go test ./coderd/notifications -run="Test.*Golden$" -update` leaves `git status --porcelain coderd/notifications/testdata` empty - `go test -race -count=2 -shuffle=on -run '^TestNotificationTemplates_Golden$' ./coderd/notifications/` passes - `golangci-lint run ./coderd/notifications/...` reports 0 issues Part of the test-suite speedup series (see #29457); independent of the other PRs. ## Remote validation Remote run on a dogfood workspace (separate compute), chat https://dogfood.cdr.dev/agents/5bbe73d0-866a-4841-9cc3-c5e5b4d97e44, tested head `e44b489be9e` against base `9d973372774` on the same machine: `coderd/notifications` 7.53s to 2.92s; `-race -count=3 -shuffle=on -run '^TestNotificationTemplates_Golden$'`: pass; `TZ=UTC go test ./coderd/notifications -run="Test.*Golden$" -update` followed by `git status --porcelain coderd/notifications/testdata`: empty. Endorsed verdict: PASS. > Xum (an AI agent) authored this PR on behalf of @ibetitsmike.
This commit is contained in:
@@ -728,15 +728,7 @@ func enumerateAllTemplates(t *testing.T) ([]string, error) {
|
||||
func TestNotificationTemplates_Golden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
username = "bob"
|
||||
password = "🤫"
|
||||
|
||||
hello = "localhost"
|
||||
|
||||
from = "system@coder.com"
|
||||
hint = "run \"make gen/golden-files\" and commit the changes"
|
||||
)
|
||||
const hint = "run \"make gen/golden-files\" and commit the changes"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -1184,7 +1176,7 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
||||
"initiator": "autobuild",
|
||||
},
|
||||
},
|
||||
appName: "Custom Application Name",
|
||||
appName: "Custom Application",
|
||||
logoURL: "https://custom.application/logo.png",
|
||||
},
|
||||
{
|
||||
@@ -1569,176 +1561,40 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
||||
require.Truef(t, found, "could not find test case for %q", name)
|
||||
}
|
||||
|
||||
// The recipient row and the appearance settings are the only deployment
|
||||
// state a golden depends on, so every template shares one pipeline unless
|
||||
// it renders different values for them.
|
||||
var templateIDs []uuid.UUID
|
||||
for _, tc := range tests {
|
||||
if !slices.Contains(templateIDs, tc.id) {
|
||||
templateIDs = append(templateIDs, tc.id)
|
||||
}
|
||||
}
|
||||
sharedUser := database.User{Name: "Bobby", Email: "bobby@coder.com", Username: "bobby"}
|
||||
shared := newGoldenFixture(t, sharedUser, "", "", templateIDs)
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fixture := shared
|
||||
sameRecipient := tc.payload.UserName == sharedUser.Name &&
|
||||
tc.payload.UserEmail == sharedUser.Email &&
|
||||
tc.payload.UserUsername == sharedUser.Username
|
||||
if !sameRecipient || tc.appName != "" || tc.logoURL != "" {
|
||||
user := database.User{Name: tc.payload.UserName, Email: tc.payload.UserEmail, Username: tc.payload.UserUsername}
|
||||
fixture = newGoldenFixture(t, user, tc.appName, tc.logoURL, templateIDs)
|
||||
}
|
||||
|
||||
t.Run("smtp", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Spin up the DB
|
||||
db, logger, user := func() (*database.Store, *slog.Logger, *codersdk.User) {
|
||||
adminClient, _, api := coderdtest.NewWithAPI(t, nil)
|
||||
db := api.Database
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
|
||||
_, user := coderdtest.CreateAnotherUserMutators(
|
||||
t,
|
||||
adminClient,
|
||||
firstUser.OrganizationID,
|
||||
[]rbac.RoleIdentifier{rbac.RoleUserAdmin()},
|
||||
func(r *codersdk.CreateUserRequestWithOrgs) {
|
||||
r.Username = tc.payload.UserUsername
|
||||
r.Email = tc.payload.UserEmail
|
||||
r.Name = tc.payload.UserName
|
||||
},
|
||||
)
|
||||
|
||||
// With the introduction of notifications that can be disabled
|
||||
// by default, we want to make sure the user preferences have
|
||||
// the notification enabled.
|
||||
_, err := adminClient.UpdateUserNotificationPreferences(
|
||||
context.Background(),
|
||||
user.ID,
|
||||
codersdk.UpdateUserNotificationPreferences{
|
||||
TemplateDisabledMap: map[string]bool{
|
||||
tc.id.String(): false,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return &db, &api.Logger, &user
|
||||
}()
|
||||
|
||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||
|
||||
_, pubsub := dbtestutil.NewDB(t)
|
||||
|
||||
// smtp config shared between client and server
|
||||
smtpConfig := codersdk.NotificationsEmailConfig{
|
||||
Hello: hello,
|
||||
From: from,
|
||||
|
||||
Auth: codersdk.NotificationsEmailAuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
|
||||
// Spin up the mock SMTP server
|
||||
backend := smtptest.NewBackend(smtptest.Config{
|
||||
AuthMechanisms: []string{sasl.Login},
|
||||
|
||||
AcceptedIdentity: smtpConfig.Auth.Identity.String(),
|
||||
AcceptedUsername: username,
|
||||
AcceptedPassword: password,
|
||||
})
|
||||
|
||||
// Create a mock SMTP server which conditionally listens for plain or TLS connections.
|
||||
srv, listen, err := smtptest.CreateMockSMTPServer(backend, false)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := srv.Shutdown(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
var hp serpent.HostPort
|
||||
require.NoError(t, hp.Set(listen.Addr().String()))
|
||||
smtpConfig.Smarthost = serpent.String(hp.String())
|
||||
|
||||
// Start mock SMTP server in the background.
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(func() {
|
||||
assert.NoError(t, srv.Serve(listen))
|
||||
})
|
||||
|
||||
// Wait for the server to become pingable.
|
||||
require.Eventually(t, func() bool {
|
||||
cl, err := smtptest.PingClient(listen, false, smtpConfig.TLS.StartTLS.Value())
|
||||
if err != nil {
|
||||
t.Logf("smtp not yet dialable: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if err = cl.Noop(); err != nil {
|
||||
t.Logf("smtp not yet noopable: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if err = cl.Close(); err != nil {
|
||||
t.Logf("smtp didn't close properly: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
|
||||
smtpCfg := defaultNotificationsConfig(database.NotificationMethodSmtp)
|
||||
smtpCfg.SMTP = smtpConfig
|
||||
|
||||
smtpManager, err := notifications.NewManager(
|
||||
smtpCfg,
|
||||
*db,
|
||||
pubsub,
|
||||
defaultHelpers(),
|
||||
createMetrics(),
|
||||
logger.Named("manager"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// we apply ApplicationName and LogoURL changes directly in the db
|
||||
// as appearance changes are enterprise features and we do not want to mix those
|
||||
// can't use the api
|
||||
if tc.appName != "" {
|
||||
err = (*db).UpsertApplicationName(ctx, "Custom Application")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if tc.logoURL != "" {
|
||||
err = (*db).UpsertLogoURL(ctx, "https://custom.application/logo.png")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
smtpManager.Run(ctx)
|
||||
|
||||
notificationCfg := defaultNotificationsConfig(database.NotificationMethodSmtp)
|
||||
|
||||
smtpEnqueuer, err := notifications.NewStoreEnqueuer(
|
||||
notificationCfg,
|
||||
*db,
|
||||
defaultHelpers(),
|
||||
logger.Named("enqueuer"),
|
||||
quartz.NewReal(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = smtpEnqueuer.EnqueueWithData(
|
||||
ctx,
|
||||
user.ID,
|
||||
tc.id,
|
||||
tc.payload.Labels,
|
||||
tc.payload.Data,
|
||||
user.Username,
|
||||
tc.payload.Targets...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the message to be fetched
|
||||
var msg *smtptest.Message
|
||||
require.Eventually(t, func() bool {
|
||||
msg = backend.LastMessage()
|
||||
return msg != nil && len(msg.Contents) > 0
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
|
||||
body := normalizeGoldenEmail([]byte(msg.Contents))
|
||||
|
||||
err = smtpManager.Stop(ctx)
|
||||
require.NoError(t, err)
|
||||
body := fixture.deliverSMTP(t, tc.id, tc.payload)
|
||||
|
||||
partialName := strings.Split(t.Name(), "/")[1]
|
||||
goldenFile := filepath.Join("testdata", "rendered-templates", "smtp", partialName+".html.golden")
|
||||
if *updateGoldenFiles {
|
||||
err = os.MkdirAll(filepath.Dir(goldenFile), 0o755)
|
||||
err := os.MkdirAll(filepath.Dir(goldenFile), 0o755)
|
||||
require.NoError(t, err, "want no error creating golden file directory")
|
||||
err = os.WriteFile(goldenFile, body, 0o600)
|
||||
require.NoError(t, err, "want no error writing body golden file")
|
||||
@@ -1757,114 +1613,12 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
||||
t.Run("webhook", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Spin up the DB
|
||||
db, logger, user := func() (*database.Store, *slog.Logger, *codersdk.User) {
|
||||
adminClient, _, api := coderdtest.NewWithAPI(t, nil)
|
||||
db := api.Database
|
||||
firstUser := coderdtest.CreateFirstUser(t, adminClient)
|
||||
|
||||
_, user := coderdtest.CreateAnotherUserMutators(
|
||||
t,
|
||||
adminClient,
|
||||
firstUser.OrganizationID,
|
||||
[]rbac.RoleIdentifier{rbac.RoleUserAdmin()},
|
||||
func(r *codersdk.CreateUserRequestWithOrgs) {
|
||||
r.Username = tc.payload.UserUsername
|
||||
r.Email = tc.payload.UserEmail
|
||||
r.Name = tc.payload.UserName
|
||||
},
|
||||
)
|
||||
|
||||
// With the introduction of notifications that can be disabled
|
||||
// by default, we want to make sure the user preferences have
|
||||
// the notification enabled.
|
||||
_, err := adminClient.UpdateUserNotificationPreferences(
|
||||
context.Background(),
|
||||
user.ID,
|
||||
codersdk.UpdateUserNotificationPreferences{
|
||||
TemplateDisabledMap: map[string]bool{
|
||||
tc.id.String(): false,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return &db, &api.Logger, &user
|
||||
}()
|
||||
|
||||
_, pubsub := dbtestutil.NewDB(t)
|
||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||
|
||||
// Spin up the mock webhook server
|
||||
var body []byte
|
||||
var readErr error
|
||||
webhookReceived := make(chan struct{})
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
body, readErr = io.ReadAll(r.Body)
|
||||
close(webhookReceived)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
endpoint, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
webhookCfg := defaultNotificationsConfig(database.NotificationMethodWebhook)
|
||||
|
||||
webhookCfg.Webhook = codersdk.NotificationsWebhookConfig{
|
||||
Endpoint: *serpent.URLOf(endpoint),
|
||||
}
|
||||
|
||||
webhookManager, err := notifications.NewManager(
|
||||
webhookCfg,
|
||||
*db,
|
||||
pubsub,
|
||||
defaultHelpers(),
|
||||
createMetrics(),
|
||||
logger.Named("manager"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
webhookManager.Run(ctx)
|
||||
|
||||
httpEnqueuer, err := notifications.NewStoreEnqueuer(
|
||||
defaultNotificationsConfig(database.NotificationMethodWebhook),
|
||||
*db,
|
||||
defaultHelpers(),
|
||||
logger.Named("enqueuer"),
|
||||
quartz.NewReal(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = httpEnqueuer.EnqueueWithData(
|
||||
ctx,
|
||||
user.ID,
|
||||
tc.id,
|
||||
tc.payload.Labels,
|
||||
tc.payload.Data,
|
||||
user.Username,
|
||||
tc.payload.Targets...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-time.After(testutil.WaitShort):
|
||||
require.Fail(t, "timed out waiting for webhook to be received")
|
||||
case <-webhookReceived:
|
||||
}
|
||||
// Handle the body that was read in the http server here.
|
||||
// We need to do it here because we can't call require.* in a separate goroutine, such as the http server handler
|
||||
require.NoError(t, readErr)
|
||||
var prettyJSON bytes.Buffer
|
||||
err = json.Indent(&prettyJSON, body, "", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
content := normalizeGoldenWebhook(prettyJSON.Bytes())
|
||||
content := fixture.deliverWebhook(t, tc.id, tc.payload)
|
||||
|
||||
partialName := strings.Split(t.Name(), "/")[1]
|
||||
goldenFile := filepath.Join("testdata", "rendered-templates", "webhook", partialName+".json.golden")
|
||||
if *updateGoldenFiles {
|
||||
err = os.MkdirAll(filepath.Dir(goldenFile), 0o755)
|
||||
err := os.MkdirAll(filepath.Dir(goldenFile), 0o755)
|
||||
require.NoError(t, err, "want no error creating golden file directory")
|
||||
err = os.WriteFile(goldenFile, content, 0o600)
|
||||
require.NoError(t, err, "want no error writing body golden file")
|
||||
@@ -1880,6 +1634,258 @@ func TestNotificationTemplates_Golden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// goldenFixture is the delivery pipeline shared by the templates of a golden
|
||||
// run: one database, one recipient, and one manager dispatching to a mock SMTP
|
||||
// server and a mock webhook server.
|
||||
type goldenFixture struct {
|
||||
ctx context.Context
|
||||
user database.User
|
||||
|
||||
smtpMu sync.Mutex
|
||||
smtpBackend *smtptest.Backend
|
||||
smtpEnqueuer *notifications.StoreEnqueuer
|
||||
|
||||
webhookMu sync.Mutex
|
||||
webhookReceived chan []byte
|
||||
webhookEnqueuer *notifications.StoreEnqueuer
|
||||
}
|
||||
|
||||
func newGoldenFixture(t *testing.T, user database.User, appName, logoURL string, templateIDs []uuid.UUID) *goldenFixture {
|
||||
t.Helper()
|
||||
|
||||
const (
|
||||
username = "bob"
|
||||
password = "🤫"
|
||||
|
||||
hello = "localhost"
|
||||
|
||||
from = "system@coder.com"
|
||||
)
|
||||
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
logger := testutil.Logger(t)
|
||||
ctx := dbauthz.AsNotifier(testutil.Context(t, testutil.WaitSuperLong))
|
||||
|
||||
user = dbgen.User(t, db, user)
|
||||
|
||||
// With the introduction of notifications that can be disabled
|
||||
// by default, we want to make sure the user preferences have
|
||||
// the notification enabled.
|
||||
_, err := db.UpdateUserNotificationPreferences(ctx, database.UpdateUserNotificationPreferencesParams{
|
||||
UserID: user.ID,
|
||||
NotificationTemplateIds: templateIDs,
|
||||
Disableds: make([]bool, len(templateIDs)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// we apply ApplicationName and LogoURL changes directly in the db
|
||||
// as appearance changes are enterprise features and we do not want to mix those
|
||||
// can't use the api
|
||||
if appName != "" {
|
||||
err = db.UpsertApplicationName(ctx, appName)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if logoURL != "" {
|
||||
err = db.UpsertLogoURL(ctx, logoURL)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// smtp config shared between client and server
|
||||
smtpConfig := codersdk.NotificationsEmailConfig{
|
||||
Hello: hello,
|
||||
From: from,
|
||||
|
||||
Auth: codersdk.NotificationsEmailAuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
},
|
||||
}
|
||||
|
||||
// Spin up the mock SMTP server
|
||||
backend := smtptest.NewBackend(smtptest.Config{
|
||||
AuthMechanisms: []string{sasl.Login},
|
||||
|
||||
AcceptedIdentity: smtpConfig.Auth.Identity.String(),
|
||||
AcceptedUsername: username,
|
||||
AcceptedPassword: password,
|
||||
})
|
||||
|
||||
// Create a mock SMTP server which conditionally listens for plain or TLS connections.
|
||||
srv, listen, err := smtptest.CreateMockSMTPServer(backend, false)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := srv.Shutdown(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
var hp serpent.HostPort
|
||||
require.NoError(t, hp.Set(listen.Addr().String()))
|
||||
smtpConfig.Smarthost = serpent.String(hp.String())
|
||||
|
||||
// Start mock SMTP server in the background.
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(func() {
|
||||
assert.NoError(t, srv.Serve(listen))
|
||||
})
|
||||
|
||||
// Wait for the server to become pingable.
|
||||
require.Eventually(t, func() bool {
|
||||
cl, err := smtptest.PingClient(listen, false, smtpConfig.TLS.StartTLS.Value())
|
||||
if err != nil {
|
||||
t.Logf("smtp not yet dialable: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if err = cl.Noop(); err != nil {
|
||||
t.Logf("smtp not yet noopable: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
if err = cl.Close(); err != nil {
|
||||
t.Logf("smtp didn't close properly: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
|
||||
// Spin up the mock webhook server
|
||||
webhookReceived := make(chan []byte, 1)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
webhookReceived <- body
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
endpoint, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The notifier acquires messages of every method, so one manager with both
|
||||
// dispatchers configured delivers the SMTP and webhook messages.
|
||||
cfg := defaultNotificationsConfig(database.NotificationMethodSmtp)
|
||||
cfg.SMTP = smtpConfig
|
||||
cfg.Webhook = codersdk.NotificationsWebhookConfig{
|
||||
Endpoint: *serpent.URLOf(endpoint),
|
||||
}
|
||||
// Deliveries are serialized per method, so the fetch interval bounds the
|
||||
// whole run.
|
||||
cfg.FetchInterval = serpent.Duration(10 * time.Millisecond)
|
||||
|
||||
manager, err := notifications.NewManager(
|
||||
cfg,
|
||||
db,
|
||||
pubsub,
|
||||
defaultHelpers(),
|
||||
createMetrics(),
|
||||
logger.Named("manager"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
manager.Run(ctx)
|
||||
t.Cleanup(func() {
|
||||
err := manager.Stop(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
smtpEnqueuer, err := notifications.NewStoreEnqueuer(
|
||||
defaultNotificationsConfig(database.NotificationMethodSmtp),
|
||||
db,
|
||||
defaultHelpers(),
|
||||
logger.Named("enqueuer"),
|
||||
quartz.NewReal(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
webhookEnqueuer, err := notifications.NewStoreEnqueuer(
|
||||
defaultNotificationsConfig(database.NotificationMethodWebhook),
|
||||
db,
|
||||
defaultHelpers(),
|
||||
logger.Named("enqueuer"),
|
||||
quartz.NewReal(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &goldenFixture{
|
||||
ctx: ctx,
|
||||
user: user,
|
||||
smtpBackend: backend,
|
||||
smtpEnqueuer: smtpEnqueuer,
|
||||
webhookReceived: webhookReceived,
|
||||
webhookEnqueuer: webhookEnqueuer,
|
||||
}
|
||||
}
|
||||
|
||||
// deliverSMTP enqueues one message and returns the normalized email received
|
||||
// for it by the mock SMTP server.
|
||||
func (f *goldenFixture) deliverSMTP(t *testing.T, templateID uuid.UUID, payload types.MessagePayload) []byte {
|
||||
t.Helper()
|
||||
|
||||
// The mock backend only records the last message, so deliveries are
|
||||
// serialized.
|
||||
f.smtpMu.Lock()
|
||||
defer f.smtpMu.Unlock()
|
||||
|
||||
f.smtpBackend.Reset()
|
||||
_, err := f.smtpEnqueuer.EnqueueWithData(
|
||||
f.ctx,
|
||||
f.user.ID,
|
||||
templateID,
|
||||
payload.Labels,
|
||||
payload.Data,
|
||||
f.user.Username,
|
||||
payload.Targets...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Wait for the message to be fetched
|
||||
var msg *smtptest.Message
|
||||
require.Eventually(t, func() bool {
|
||||
msg = f.smtpBackend.LastMessage()
|
||||
return msg != nil && len(msg.Contents) > 0
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
|
||||
return normalizeGoldenEmail([]byte(msg.Contents))
|
||||
}
|
||||
|
||||
// deliverWebhook enqueues one message and returns the normalized body the mock
|
||||
// webhook server received for it.
|
||||
func (f *goldenFixture) deliverWebhook(t *testing.T, templateID uuid.UUID, payload types.MessagePayload) []byte {
|
||||
t.Helper()
|
||||
|
||||
// A single receive channel cannot attribute bodies to templates, so
|
||||
// deliveries are serialized.
|
||||
f.webhookMu.Lock()
|
||||
defer f.webhookMu.Unlock()
|
||||
|
||||
_, err := f.webhookEnqueuer.EnqueueWithData(
|
||||
f.ctx,
|
||||
f.user.ID,
|
||||
templateID,
|
||||
payload.Labels,
|
||||
payload.Data,
|
||||
f.user.Username,
|
||||
payload.Targets...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
var body []byte
|
||||
select {
|
||||
case <-time.After(testutil.WaitShort):
|
||||
require.Fail(t, "timed out waiting for webhook to be received")
|
||||
case body = <-f.webhookReceived:
|
||||
}
|
||||
|
||||
var prettyJSON bytes.Buffer
|
||||
err = json.Indent(&prettyJSON, body, "", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
return normalizeGoldenWebhook(prettyJSON.Bytes())
|
||||
}
|
||||
|
||||
// normalizeLineEndings ensures that all line endings are normalized to \n.
|
||||
// Required for Windows compatibility.
|
||||
func normalizeLineEndings(content []byte) []byte {
|
||||
|
||||
Reference in New Issue
Block a user