Files
zitadel/internal/notification/handlers/config_email_test.go
T
8e82ec1cb9 Merge commit from fork
* Add DenyLists parsing

* Remove unneeded returned error

* Plug global denylist into Command

* app creation: apply denylist to backchannel logout URI

* Inject denylist to backchannel logout worker

* webhook config: validate against blocked URLs

* Add notificationsWebhook denylist target

* command: Add SMTP endpoint validation against blocklist

* command: Add SMS endpoint validation against blocklist

* Validate webhook endpoint against denylist on channel notification

* Remove unused tests

* handle deprecated denylists

* remove unintended denylist entry in deprecated list

* use single http client

* fix tests

* update comments

* fixes

* cleanup

* address comments

* fix merge

---------

Co-authored-by: Livio Spring <[email protected]>
2026-06-15 15:36:14 +02:00

358 lines
10 KiB
Go

package handlers
import (
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/notification/channels/email"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
"github.com/zitadel/zitadel/internal/notification/handlers/mock"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
t.Parallel()
const instId = "instance-1"
const cryptAlg = "alg1"
const keyId = "key1"
const pwd = "password"
const clientSecret = "client-secret"
encryptedPwd := []byte("password-encrypted")
encryptedClientSecret := []byte("client-secret-encrypted")
var (
errDecryptionFailed = errors.New("decryption failed")
errDatabaseError = errors.New("database error")
)
tt := []struct {
name string
smtpConfig *query.SMTPConfig
smtpConfigErr error
setupCrypto func(m *crypto.MockEncryptionAlgorithm)
expected *email.Config
expectedErr error
}{
{
name: "smtp config with plain auth (user and password)",
smtpConfig: &query.SMTPConfig{
ID: "smtp1",
Description: "smtp config with plain auth (user and password)",
SMTPConfig: &query.SMTP{
TLS: true,
SenderAddress: "[email protected]",
SenderName: "sender",
ReplyToAddress: "[email protected]",
Host: "mail.com",
User: "mail-user",
PlainAuth: &query.PlainAuth{
Password: &crypto.CryptoValue{
CryptoType: 0,
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedPwd,
},
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(pwd, nil)
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp1",
Description: "smtp config with plain auth (user and password)",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
Host: "mail.com",
PlainAuth: &smtp.PlainAuthConfig{
User: "mail-user",
Password: pwd,
},
},
Tls: true,
From: "[email protected]",
FromName: "sender",
ReplyToAddress: "[email protected]",
},
},
},
{
name: "smtp config with user but no plain auth config (fallback to plain auth without password)",
smtpConfig: &query.SMTPConfig{
ID: "smtp2",
Description: "smtp config with user but no plain auth config (fallback to plain auth without password)",
SMTPConfig: &query.SMTP{
TLS: true,
SenderAddress: "[email protected]",
SenderName: "sender",
ReplyToAddress: "[email protected]",
Host: "mail.com",
User: "mail-user",
},
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp2",
Description: "smtp config with user but no plain auth config (fallback to plain auth without password)",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
Host: "mail.com",
PlainAuth: &smtp.PlainAuthConfig{
User: "mail-user",
},
},
Tls: true,
From: "[email protected]",
FromName: "sender",
ReplyToAddress: "[email protected]",
},
},
},
{
name: "smtp config without authentication",
smtpConfig: &query.SMTPConfig{
ID: "smtp3",
Description: "smtp config without authentication",
SMTPConfig: &query.SMTP{
TLS: true,
SenderAddress: "[email protected]",
SenderName: "sender",
ReplyToAddress: "[email protected]",
Host: "mail.com",
},
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp3",
Description: "smtp config without authentication",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
Host: "mail.com",
},
Tls: true,
From: "[email protected]",
FromName: "sender",
ReplyToAddress: "[email protected]",
},
},
},
{
name: "smtp config with xoauth2 auth and client credentials",
smtpConfig: &query.SMTPConfig{
ID: "smtp4",
Description: "smtp config with xoauth2 auth and client credentials",
SMTPConfig: &query.SMTP{
SenderAddress: "[email protected]",
SenderName: "sender",
Host: "mail.com",
User: "mail-user",
XOAuth2Auth: &query.XOAuth2Auth{
TokenEndpoint: "https://token.example.com/token",
Scopes: []string{"https://mail.example.com/.default"},
ClientCredentials: &query.XOAuthClientCredentials{
ClientId: "client-id",
ClientSecret: &crypto.CryptoValue{
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedClientSecret,
},
},
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(clientSecret, nil)
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp4",
Description: "smtp config with xoauth2 auth and client credentials",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
Host: "mail.com",
XOAuth2Auth: &smtp.XOAuth2AuthConfig{
User: "mail-user",
TokenEndpoint: "https://token.example.com/token",
Scopes: []string{"https://mail.example.com/.default"},
ClientCredentialsAuth: &smtp.OAuth2ClientCredentials{
ClientId: "client-id",
ClientSecret: clientSecret,
},
},
},
From: "[email protected]",
FromName: "sender",
},
},
},
{
name: "smtp config with xoauth2 auth without client credentials",
smtpConfig: &query.SMTPConfig{
ID: "smtp5",
Description: "smtp config with xoauth2 auth without client credentials",
SMTPConfig: &query.SMTP{
SenderAddress: "[email protected]",
SenderName: "sender",
Host: "mail.com",
User: "mail-user",
XOAuth2Auth: &query.XOAuth2Auth{
TokenEndpoint: "https://token.example.com/token",
Scopes: []string{"scope1"},
},
},
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp5",
Description: "smtp config with xoauth2 auth without client credentials",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
Host: "mail.com",
XOAuth2Auth: &smtp.XOAuth2AuthConfig{
User: "mail-user",
TokenEndpoint: "https://token.example.com/token",
Scopes: []string{"scope1"},
},
},
From: "[email protected]",
FromName: "sender",
},
},
},
{
name: "plain auth password decryption error",
smtpConfig: &query.SMTPConfig{
ID: "smtp6",
Description: "plain auth password decryption error",
SMTPConfig: &query.SMTP{
Host: "mail.com",
User: "mail-user",
PlainAuth: &query.PlainAuth{
Password: &crypto.CryptoValue{
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedPwd,
},
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("", errDecryptionFailed)
},
expectedErr: errDecryptionFailed,
},
{
name: "xoauth2 client credentials decryption error",
smtpConfig: &query.SMTPConfig{
ID: "smtp7",
Description: "xoauth2 client credentials decryption error",
SMTPConfig: &query.SMTP{
Host: "mail.com",
User: "mail-user",
XOAuth2Auth: &query.XOAuth2Auth{
TokenEndpoint: "https://token.example.com/token",
ClientCredentials: &query.XOAuthClientCredentials{
ClientId: "client-id",
ClientSecret: &crypto.CryptoValue{
CryptoType: 0,
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedClientSecret,
},
},
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("", errDecryptionFailed)
},
expectedErr: errDecryptionFailed,
},
{
name: "http webhook config",
smtpConfig: &query.SMTPConfig{
ID: "http1",
Description: "http webhook config",
HTTPConfig: &query.HTTP{
Endpoint: "https://example.com/notify",
SigningKey: "my-signing-key",
},
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "http1",
Description: "http webhook config",
},
WebhookConfig: &webhook.Config{
CallURL: "https://example.com/notify",
Method: http.MethodPost,
SigningKey: "my-signing-key",
},
},
},
{
name: "neither smtp nor http config set",
smtpConfig: &query.SMTPConfig{
ID: "smtp8",
Description: "neither smtp nor http config set",
},
expectedErr: zerrors.ThrowNotFound(nil, "", ""),
},
{
name: "SMTPConfigActive query error",
smtpConfigErr: errDatabaseError,
expectedErr: errDatabaseError,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext(instId, "org-1", "user-1")
cryptAlgMock := crypto.NewMockEncryptionAlgorithm(ctrl)
if tc.setupCrypto != nil {
tc.setupCrypto(cryptAlgMock)
}
queryMock := mock.NewMockQueries(ctrl)
queryMock.EXPECT().SMTPConfigActive(gomock.Any(), instId).Return(tc.smtpConfig, tc.smtpConfigErr)
notificationQueries := NewNotificationQueries(queryMock, &eventstore.Eventstore{}, "ext domain", uint16(1234), false, "filepath", nil, cryptAlgMock, nil, nil)
cfg, err := notificationQueries.GetActiveEmailConfig(ctx)
assert.ErrorIs(t, err, tc.expectedErr)
if tc.expectedErr != nil {
return
}
assert.EqualValues(t, tc.expected, cfg)
})
}
}