- Implemented `useManualChallenge`, `useChallengePoll`, and `useManualChallengeMutations` hooks for managing manual DNS challenges. - Created tests for the `useManualChallenge` hooks to ensure correct fetching and mutation behavior. - Added `ManualDNSChallenge` component for displaying challenge details and actions. - Developed end-to-end tests for the Manual DNS Provider feature, covering provider selection, challenge UI, and accessibility compliance. - Included error handling tests for verification failures and network errors.
396 lines
14 KiB
Go
396 lines
14 KiB
Go
package caddy
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestImporter_ExtractHosts_TLSConnectionPolicyAndDialWithoutPort(t *testing.T) {
|
|
// Build a sample Caddy JSON with TLSConnectionPolicies and reverse_proxy with dial host:port and host-only dials
|
|
cfg := CaddyConfig{
|
|
Apps: &CaddyApps{
|
|
HTTP: &CaddyHTTP{
|
|
Servers: map[string]*CaddyServer{
|
|
"srv": {
|
|
Listen: []string{":443"},
|
|
Routes: []*CaddyRoute{
|
|
{
|
|
Match: []*CaddyMatcher{{Host: []string{"example.com"}}},
|
|
Handle: []*CaddyHandler{
|
|
{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "app:9000"}}},
|
|
},
|
|
},
|
|
{
|
|
Match: []*CaddyMatcher{{Host: []string{"nport.example.com"}}},
|
|
Handle: []*CaddyHandler{
|
|
{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "app"}}},
|
|
},
|
|
},
|
|
},
|
|
TLSConnectionPolicies: struct{}{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
out, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(out)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 2)
|
|
// First host should have scheme https because Listen :443
|
|
require.Equal(t, "https", res.Hosts[0].ForwardScheme)
|
|
// second host with dial 'app' should be parsed with default port 80
|
|
require.Equal(t, 80, res.Hosts[1].ForwardPort)
|
|
}
|
|
|
|
func TestExtractHandlers_Subroute_WithUnsupportedSubhandle(t *testing.T) {
|
|
// Build a handler with subroute whose handle contains a non-map item
|
|
h := []*CaddyHandler{
|
|
{Handler: "subroute", Routes: []any{map[string]any{"handle": []any{"not-a-map", map[string]any{"handler": "reverse_proxy"}}}}},
|
|
}
|
|
importer := NewImporter("")
|
|
res := importer.extractHandlers(h)
|
|
// Should ignore the non-map and keep the reverse_proxy handler
|
|
require.Len(t, res, 1)
|
|
require.Equal(t, "reverse_proxy", res[0].Handler)
|
|
}
|
|
|
|
func TestExtractHandlers_Subroute_WithNonMapRoutes(t *testing.T) {
|
|
h := []*CaddyHandler{
|
|
{Handler: "subroute", Routes: []any{"not-a-map"}},
|
|
}
|
|
importer := NewImporter("")
|
|
res := importer.extractHandlers(h)
|
|
require.Len(t, res, 0)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_UpstreamsNonMapAndWarnings(t *testing.T) {
|
|
cfg := CaddyConfig{
|
|
Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"warn.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{"nonnmap"}}, {Handler: "rewrite"}, {Handler: "file_server"}},
|
|
}},
|
|
}}}},
|
|
}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
require.Contains(t, res.Hosts[0].Warnings[0], "Rewrite rules not supported")
|
|
require.Contains(t, res.Hosts[0].Warnings[1], "File server directives not supported")
|
|
}
|
|
|
|
func TestBackupCaddyfile_ReadFailure(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
// original file does not exist
|
|
_, err := BackupCaddyfile("/does/not/exist", tmp)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestExtractHandlers_Subroute_EmptyAndHandleNotArray(t *testing.T) {
|
|
// Empty routes array
|
|
h := []*CaddyHandler{
|
|
{Handler: "subroute", Routes: []any{}},
|
|
}
|
|
importer := NewImporter("")
|
|
res := importer.extractHandlers(h)
|
|
require.Len(t, res, 0)
|
|
|
|
// Routes with a map but handle is not an array
|
|
h2 := []*CaddyHandler{
|
|
{Handler: "subroute", Routes: []any{map[string]any{"handle": "not-an-array"}}},
|
|
}
|
|
res2 := importer.extractHandlers(h2)
|
|
require.Len(t, res2, 0)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_ReverseProxyNoUpstreams(t *testing.T) {
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"noups.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
// No upstreams should leave ForwardHost empty and ForwardPort 0
|
|
require.Equal(t, "", res.Hosts[0].ForwardHost)
|
|
require.Equal(t, 0, res.Hosts[0].ForwardPort)
|
|
}
|
|
|
|
func TestBackupCaddyfile_Success(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
originalFile := filepath.Join(tmp, "Caddyfile")
|
|
data := []byte("original-data")
|
|
_ = os.WriteFile(originalFile, data, 0o644)
|
|
backupDir := filepath.Join(tmp, "backup")
|
|
path, err := BackupCaddyfile(originalFile, backupDir)
|
|
require.NoError(t, err)
|
|
// Backup file should exist and contain same data
|
|
b, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
require.Equal(t, data, b)
|
|
}
|
|
|
|
func TestExtractHandlers_Subroute_WithHeadersUpstreams(t *testing.T) {
|
|
h := []*CaddyHandler{
|
|
{Handler: "subroute", Routes: []any{map[string]any{"handle": []any{map[string]any{"handler": "reverse_proxy", "upstreams": []any{map[string]any{"dial": "app:8080"}}, "headers": map[string]any{"Upgrade": []any{"websocket"}}}}}}},
|
|
}
|
|
importer := NewImporter("")
|
|
res := importer.extractHandlers(h)
|
|
require.Len(t, res, 1)
|
|
require.Equal(t, "reverse_proxy", res[0].Handler)
|
|
// Upstreams should be present in extracted handler
|
|
_, ok := res[0].Upstreams.([]any)
|
|
require.True(t, ok)
|
|
_, ok = res[0].Headers.(map[string]any)
|
|
require.True(t, ok)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_DuplicateHost(t *testing.T) {
|
|
cfg := CaddyConfig{
|
|
Apps: &CaddyApps{
|
|
HTTP: &CaddyHTTP{
|
|
Servers: map[string]*CaddyServer{
|
|
"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"dup.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "one:80"}}}},
|
|
}},
|
|
},
|
|
"srv2": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"dup.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "two:80"}}}},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
// Duplicate should be captured in Conflicts
|
|
require.Len(t, res.Conflicts, 1)
|
|
require.Equal(t, "dup.example.com", res.Conflicts[0])
|
|
}
|
|
|
|
func TestBackupCaddyfile_WriteFailure(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
originalFile := filepath.Join(tmp, "Caddyfile")
|
|
_ = os.WriteFile(originalFile, []byte("original"), 0o644)
|
|
// Create backup dir and make it readonly to prevent writing (best-effort)
|
|
backupDir := filepath.Join(tmp, "backup")
|
|
_ = os.MkdirAll(backupDir, 0o555)
|
|
_, err := BackupCaddyfile(originalFile, backupDir)
|
|
// Might error due to write permission; accept both success or failure depending on platform
|
|
if err != nil {
|
|
require.Error(t, err)
|
|
} else {
|
|
entries, _ := os.ReadDir(backupDir)
|
|
require.True(t, len(entries) > 0)
|
|
}
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_SSLForcedByDomainScheme(t *testing.T) {
|
|
// Domain contains scheme prefix, which should set SSLForced
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"https://secure.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "one:80"}}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
require.Equal(t, true, res.Hosts[0].SSLForced)
|
|
require.Equal(t, "https", res.Hosts[0].ForwardScheme)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_MultipleHostsInMatch(t *testing.T) {
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"m1.example.com", "m2.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "one:80"}}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 2)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_UpgradeHeaderAsString(t *testing.T) {
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"ws.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "one:80"}}, Headers: map[string]any{"Upgrade": []string{"websocket"}}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
// Websocket support should be detected after JSON roundtrip
|
|
require.True(t, res.Hosts[0].WebsocketSupport)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_SscanfFailureOnPort(t *testing.T) {
|
|
// Trigger net.SplitHostPort success but Sscanf failing
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"sscanf.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "127.0.0.1:eighty"}}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
// Sscanf should fail and default to port 80
|
|
require.Equal(t, 80, res.Hosts[0].ForwardPort)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_PartsSscanfFail(t *testing.T) {
|
|
// Trigger net.SplitHostPort fail but strings.Split parts with non-numeric port
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"parts.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "tcp/127.0.0.1:badport"}}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
require.Equal(t, 80, res.Hosts[0].ForwardPort)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_PartsEmptyPortField(t *testing.T) {
|
|
// net.SplitHostPort fails (missing port) but strings.Split returns two parts with empty port
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"emptyparts.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "tcp/127.0.0.1:"}}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
require.Equal(t, 80, res.Hosts[0].ForwardPort)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_ForceSplitFallback_PartsNumericPort(t *testing.T) {
|
|
// Force the fallback split behavior to hit len(parts)==2 branch
|
|
orig := forceSplitFallback
|
|
forceSplitFallback = true
|
|
defer func() { forceSplitFallback = orig }()
|
|
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"forced.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "127.0.0.1:8181"}}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
require.Equal(t, "127.0.0.1", res.Hosts[0].ForwardHost)
|
|
require.Equal(t, 8181, res.Hosts[0].ForwardPort)
|
|
}
|
|
|
|
func TestImporter_ExtractHosts_ForceSplitFallback_PartsSscanfFail(t *testing.T) {
|
|
// Force the fallback split behavior with non-numeric port to hit Sscanf error branch
|
|
orig := forceSplitFallback
|
|
forceSplitFallback = true
|
|
defer func() { forceSplitFallback = orig }()
|
|
|
|
cfg := CaddyConfig{Apps: &CaddyApps{HTTP: &CaddyHTTP{Servers: map[string]*CaddyServer{"srv": {
|
|
Listen: []string{":80"},
|
|
Routes: []*CaddyRoute{{
|
|
Match: []*CaddyMatcher{{Host: []string{"forcedfail.example.com"}}},
|
|
Handle: []*CaddyHandler{{Handler: "reverse_proxy", Upstreams: []any{map[string]any{"dial": "127.0.0.1:notnum"}}}},
|
|
}},
|
|
}}}}}
|
|
b, _ := json.Marshal(cfg)
|
|
importer := NewImporter("")
|
|
res, err := importer.ExtractHosts(b)
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Hosts, 1)
|
|
require.Equal(t, 80, res.Hosts[0].ForwardPort)
|
|
}
|
|
|
|
func TestBackupCaddyfile_WriteErrorDeterministic(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
originalFile := filepath.Join(tmp, "Caddyfile")
|
|
_ = os.WriteFile(originalFile, []byte("original-data"), 0o644)
|
|
backupDir := filepath.Join(tmp, "backup")
|
|
_ = os.MkdirAll(backupDir, 0o755)
|
|
// Determine backup path name the function will use
|
|
pid := fmt.Sprintf("%d", os.Getpid())
|
|
// Pre-create a directory at the exact backup path to ensure write fails with EISDIR
|
|
path := filepath.Join(backupDir, fmt.Sprintf("Caddyfile.%s.backup", pid))
|
|
_ = os.Mkdir(path, 0o755)
|
|
_, err := BackupCaddyfile(originalFile, backupDir)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestParseCaddyfile_InvalidPath(t *testing.T) {
|
|
importer := NewImporter("")
|
|
_, err := importer.ParseCaddyfile("")
|
|
require.Error(t, err)
|
|
|
|
_, err = importer.ParseCaddyfile(".")
|
|
require.Error(t, err)
|
|
|
|
// Path traversal should be rejected
|
|
traversal := filepath.Join("..", "Caddyfile")
|
|
_, err = importer.ParseCaddyfile(traversal)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestBackupCaddyfile_InvalidOriginalPath(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
// Empty path
|
|
_, err := BackupCaddyfile("", tmp)
|
|
require.Error(t, err)
|
|
|
|
// Path traversal rejection
|
|
_, err = BackupCaddyfile(filepath.Join("..", "Caddyfile"), tmp)
|
|
require.Error(t, err)
|
|
}
|