test: add SMTP configuration tests and multi-credential DNS provider support

This commit is contained in:
GitHub Actions
2026-01-09 07:02:36 +00:00
parent 04532efa05
commit b28f3b8bcc
6 changed files with 882 additions and 2668 deletions
@@ -3,20 +3,50 @@ package caddy
import (
"os"
"testing"
"time"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/Wikid82/charon/backend/pkg/dnsprovider"
"github.com/stretchr/testify/require"
_ "github.com/Wikid82/charon/backend/pkg/dnsprovider/builtin" // Auto-register DNS providers
)
type multiCredTestProvider struct{}
func (p *multiCredTestProvider) Type() string { return "testmulti" }
func (p *multiCredTestProvider) Metadata() dnsprovider.ProviderMetadata {
return dnsprovider.ProviderMetadata{Type: p.Type(), Name: "Test Multi", IsBuiltIn: true}
}
func (p *multiCredTestProvider) Init() error { return nil }
func (p *multiCredTestProvider) Cleanup() error { return nil }
func (p *multiCredTestProvider) RequiredCredentialFields() []dnsprovider.CredentialFieldSpec {
return nil
}
func (p *multiCredTestProvider) OptionalCredentialFields() []dnsprovider.CredentialFieldSpec {
return nil
}
func (p *multiCredTestProvider) ValidateCredentials(creds map[string]string) error { return nil }
func (p *multiCredTestProvider) TestCredentials(creds map[string]string) error { return nil }
func (p *multiCredTestProvider) SupportsMultiCredential() bool { return true }
func (p *multiCredTestProvider) BuildCaddyConfig(creds map[string]string) map[string]any {
return map[string]any{"name": p.Type(), "token": creds["token"]}
}
func (p *multiCredTestProvider) BuildCaddyConfigForZone(baseDomain string, creds map[string]string) map[string]any {
return map[string]any{"name": p.Type(), "zone": baseDomain, "token": creds["token"]}
}
func (p *multiCredTestProvider) PropagationTimeout() time.Duration { return 2 * time.Second }
func (p *multiCredTestProvider) PollingInterval() time.Duration { return 1 * time.Second }
func mustProviderID(v uint) *uint { return &v }
func TestGenerateConfig_DNSChallenge_LetsEncrypt_StagingCAAndPropagationTimeout(t *testing.T) {
providerID := uint(1)
host := models.ProxyHost{
Enabled: true,
DomainNames: "*.example.com,example.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -86,7 +116,7 @@ func TestGenerateConfig_DNSChallenge_ZeroSSL_IssuerShape(t *testing.T) {
Enabled: true,
DomainNames: "*.example.net",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -137,7 +167,7 @@ func TestGenerateConfig_DNSChallenge_SkipsPolicyWhenProviderConfigMissing(t *tes
Enabled: true,
DomainNames: "*.example.org",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -229,7 +259,7 @@ func TestGenerateConfig_MultiCredential_ZoneSpecificPolicies(t *testing.T) {
Enabled: true,
DomainNames: "*.zone1.com,zone1.com,*.zone2.com,zone2.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -284,7 +314,7 @@ func TestGenerateConfig_MultiCredential_ZeroSSL_Issuer(t *testing.T) {
Enabled: true,
DomainNames: "*.zerossl-test.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -337,7 +367,7 @@ func TestGenerateConfig_MultiCredential_BothIssuers(t *testing.T) {
Enabled: true,
DomainNames: "*.both-test.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -394,7 +424,7 @@ func TestGenerateConfig_MultiCredential_ACMEStaging(t *testing.T) {
Enabled: true,
DomainNames: "*.staging-test.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -449,7 +479,7 @@ func TestGenerateConfig_MultiCredential_NoMatchingDomains(t *testing.T) {
Enabled: true,
DomainNames: "*.actual.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -496,7 +526,7 @@ func TestGenerateConfig_MultiCredential_ProviderTypeNotFound(t *testing.T) {
Enabled: true,
DomainNames: "*.unknown-provider.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "nonexistent_provider"},
DNSProviderID: func() *uint { v := providerID; return &v }(),
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
@@ -525,3 +555,338 @@ func TestGenerateConfig_MultiCredential_ProviderTypeNotFound(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, conf)
}
func TestGenerateConfig_MultiCredential_SupportsMultiCredential_UsesZoneConfigAndStagingBothIssuers(t *testing.T) {
if err := dnsprovider.Global().Register(&multiCredTestProvider{}); err == nil {
t.Cleanup(func() { dnsprovider.Global().Unregister("testmulti") })
}
providerID := uint(16)
host := models.ProxyHost{
Enabled: true,
DomainNames: "*.z1.com,z1.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "testmulti"},
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
[]models.ProxyHost{host},
t.TempDir(),
"acme@example.com",
"",
"both",
true,
false, false, false, false,
"",
nil,
nil,
nil,
&models.SecurityConfig{},
[]DNSProviderConfig{{
ID: providerID,
ProviderType: "testmulti",
UseMultiCredentials: true,
ZoneCredentials: map[string]map[string]string{
"z1.com": {"token": "tok-z1"},
},
}},
)
require.NoError(t, err)
require.NotNil(t, conf)
require.NotNil(t, conf.Apps.TLS)
require.NotNil(t, conf.Apps.TLS.Automation)
require.NotEmpty(t, conf.Apps.TLS.Automation.Policies)
var foundCA string
var foundProvider map[string]any
for _, p := range conf.Apps.TLS.Automation.Policies {
if p == nil {
continue
}
for _, it := range p.IssuersRaw {
issuer, ok := it.(map[string]any)
if !ok || issuer["module"] != "acme" {
continue
}
if ca, ok := issuer["ca"].(string); ok {
foundCA = ca
}
ch, _ := issuer["challenges"].(map[string]any)
dnsCh, _ := ch["dns"].(map[string]any)
prov, _ := dnsCh["provider"].(map[string]any)
foundProvider = prov
break
}
if foundProvider != nil {
break
}
}
require.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", foundCA)
require.NotNil(t, foundProvider)
require.Equal(t, "z1.com", foundProvider["zone"], "expected zone-specific provider config")
}
func TestGenerateConfig_DNSChallenge_SingleCredential_BothIssuers_ACMEStaging(t *testing.T) {
providerID := uint(17)
host := models.ProxyHost{
Enabled: true,
DomainNames: "*.both-staging.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
[]models.ProxyHost{host},
t.TempDir(),
"acme@example.com",
"",
"both",
true,
false, false, false, false,
"",
nil,
nil,
nil,
&models.SecurityConfig{},
[]DNSProviderConfig{{
ID: providerID,
ProviderType: "cloudflare",
PropagationTimeout: 1,
Credentials: map[string]string{"api_token": "tok"},
}},
)
require.NoError(t, err)
require.NotNil(t, conf)
var foundIssuer map[string]any
for _, p := range conf.Apps.TLS.Automation.Policies {
if p == nil {
continue
}
for _, s := range p.Subjects {
if s != "*.both-staging.com" {
continue
}
for _, it := range p.IssuersRaw {
if m, ok := it.(map[string]any); ok && m["module"] == "acme" {
foundIssuer = m
break
}
}
}
if foundIssuer != nil {
break
}
}
require.NotNil(t, foundIssuer)
require.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", foundIssuer["ca"])
}
func TestGenerateConfig_DNSChallenge_SingleCredential_ProviderTypeNotFound_SkipsPolicy(t *testing.T) {
providerID := uint(18)
host := models.ProxyHost{
Enabled: true,
DomainNames: "*.missing-registry.com",
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "not_registered"},
DNSProviderID: mustProviderID(providerID),
}
conf, err := GenerateConfig(
[]models.ProxyHost{host},
t.TempDir(),
"acme@example.com",
"",
"letsencrypt",
false,
false, false, false, false,
"",
nil,
nil,
nil,
&models.SecurityConfig{},
[]DNSProviderConfig{{
ID: providerID,
ProviderType: "not_registered",
PropagationTimeout: 1,
Credentials: map[string]string{"token": "x"},
}},
)
require.NoError(t, err)
require.NotNil(t, conf)
}
func TestGenerateConfig_DefaultPolicy_LetsEncrypt_StagingCA(t *testing.T) {
host := models.ProxyHost{Enabled: true, DomainNames: "192.0.2.1"}
conf, err := GenerateConfig(
[]models.ProxyHost{host},
t.TempDir(),
"acme@example.com",
"",
"letsencrypt",
true,
false, false, false, false,
"",
nil,
nil,
nil,
&models.SecurityConfig{},
nil,
)
require.NoError(t, err)
require.NotNil(t, conf)
require.NotNil(t, conf.Apps.TLS)
require.NotNil(t, conf.Apps.TLS.Automation)
require.NotEmpty(t, conf.Apps.TLS.Automation.Policies)
found := false
for _, p := range conf.Apps.TLS.Automation.Policies {
if p == nil {
continue
}
for _, it := range p.IssuersRaw {
if m, ok := it.(map[string]any); ok && m["module"] == "acme" {
require.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", m["ca"])
found = true
break
}
}
if found {
break
}
}
require.True(t, found)
}
func TestGenerateConfig_DefaultPolicy_ZeroSSL_Issuer(t *testing.T) {
host := models.ProxyHost{Enabled: true, DomainNames: "192.0.2.2"}
conf, err := GenerateConfig(
[]models.ProxyHost{host},
t.TempDir(),
"acme@example.com",
"",
"zerossl",
false,
false, false, false, false,
"",
nil,
nil,
nil,
&models.SecurityConfig{},
nil,
)
require.NoError(t, err)
require.NotNil(t, conf)
require.NotNil(t, conf.Apps.TLS)
require.NotNil(t, conf.Apps.TLS.Automation)
found := false
for _, p := range conf.Apps.TLS.Automation.Policies {
if p == nil {
continue
}
for _, it := range p.IssuersRaw {
if m, ok := it.(map[string]any); ok && m["module"] == "zerossl" {
found = true
break
}
}
if found {
break
}
}
require.True(t, found)
}
func TestGenerateConfig_DefaultPolicy_BothIssuers_StagingCA(t *testing.T) {
host := models.ProxyHost{Enabled: true, DomainNames: "192.0.2.3"}
conf, err := GenerateConfig(
[]models.ProxyHost{host},
t.TempDir(),
"acme@example.com",
"",
"",
true,
false, false, false, false,
"",
nil,
nil,
nil,
&models.SecurityConfig{},
nil,
)
require.NoError(t, err)
require.NotNil(t, conf)
caFound := false
zeroFound := false
for _, p := range conf.Apps.TLS.Automation.Policies {
if p == nil {
continue
}
for _, it := range p.IssuersRaw {
m, ok := it.(map[string]any)
if !ok {
continue
}
switch m["module"] {
case "acme":
if m["ca"] == "https://acme-staging-v02.api.letsencrypt.org/directory" {
caFound = true
}
case "zerossl":
zeroFound = true
}
}
}
require.True(t, caFound)
require.True(t, zeroFound)
}
func TestGenerateConfig_IPSubjects_InitializesTLSAppAndAutomation(t *testing.T) {
providerID := uint(19)
host := models.ProxyHost{
Enabled: true,
UUID: "ip-host",
DomainNames: "1.2.3.4",
ForwardHost: "app",
ForwardPort: 8080,
DNSProvider: &models.DNSProvider{ID: providerID, ProviderType: "cloudflare"},
}
conf, err := GenerateConfig(
[]models.ProxyHost{host},
t.TempDir(),
"",
"",
"",
false,
false, false, false, false,
"",
nil,
nil,
nil,
&models.SecurityConfig{},
nil,
)
require.NoError(t, err)
require.NotNil(t, conf)
require.NotNil(t, conf.Apps.TLS)
require.NotNil(t, conf.Apps.TLS.Automation)
require.NotEmpty(t, conf.Apps.TLS.Automation.Policies)
}
func TestGetAccessLogPath_DockerEnv_UsesCrowdSecPath(t *testing.T) {
const dockerMarker = "/.dockerenv"
if err := os.WriteFile(dockerMarker, []byte("test"), 0o600); err != nil {
t.Skipf("cannot create %s: %v", dockerMarker, err)
}
t.Cleanup(func() { _ = os.Remove(dockerMarker) })
path := getAccessLogPath(t.TempDir(), false)
require.Equal(t, "/var/log/caddy/access.log", path)
}