@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 10, 23:45 UTC

master
go 366 lines 11 KB
Raw
1 package caddycmd
2
3 import (
4 "bytes"
5 "io"
6 "maps"
7 "os"
8 "path/filepath"
9 "reflect"
10 "slices"
11 "strings"
12 "testing"
13
14 "github.com/spf13/cobra"
15 "github.com/spf13/pflag"
16
17 "github.com/caddyserver/caddy/v2"
18 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
19 "github.com/caddyserver/caddy/v2/internal"
20 )
21
22 // newFmtFlags builds a Flags value wired up the same way the fmt cobra command does.
23 func newFmtFlags(configFile string, overwrite, diff, imports bool) Flags {
24 fs := pflag.NewFlagSet("fmt", pflag.ContinueOnError)
25 fs.StringP("config", "c", "", "")
26 fs.BoolP("overwrite", "w", false, "")
27 fs.BoolP("diff", "d", false, "")
28 fs.Bool("imports", false, "")
29 if configFile != "" {
30 _ = fs.Parse([]string{configFile})
31 }
32 if overwrite {
33 _ = fs.Set("overwrite", "true")
34 }
35 if diff {
36 _ = fs.Set("diff", "true")
37 }
38 if imports {
39 _ = fs.Set("imports", "true")
40 }
41 return Flags{fs}
42 }
43
44 func writeTestFile(t *testing.T, path, content string) {
45 t.Helper()
46 if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
47 t.Fatal(err)
48 }
49 }
50
51 func canonicalTestPath(t *testing.T, path string) string {
52 t.Helper()
53 path, err := filepath.EvalSymlinks(path)
54 if err != nil {
55 t.Fatal(err)
56 }
57 return path
58 }
59
60 func captureStdout(t *testing.T, fn func() (int, error)) (string, int, error) {
61 t.Helper()
62 r, w, err := os.Pipe()
63 if err != nil {
64 t.Fatal(err)
65 }
66 oldStdout := os.Stdout
67 os.Stdout = w
68 t.Cleanup(func() { os.Stdout = oldStdout })
69
70 code, callErr := fn()
71 if err := w.Close(); err != nil {
72 t.Fatal(err)
73 }
74 os.Stdout = oldStdout
75 output, err := io.ReadAll(r)
76 if err != nil {
77 t.Fatal(err)
78 }
79 if err := r.Close(); err != nil {
80 t.Fatal(err)
81 }
82 return string(output), code, callErr
83 }
84
85 // TestCmdFmtImportsStdinRejected verifies that --imports with stdin is rejected.
86 func TestCmdFmtImportsStdinRejected(t *testing.T) {
87 fl := newFmtFlags("-", false, false, true)
88 code, err := cmdFmt(fl)
89 if err == nil {
90 t.Fatal("expected an error, got nil")
91 }
92 if !strings.Contains(err.Error(), "cannot use --imports") {
93 t.Errorf("unexpected error message: %v", err)
94 }
95 if code != caddy.ExitCodeFailedStartup {
96 t.Errorf("expected ExitCodeFailedStartup, got %d", code)
97 }
98 }
99
100 // TestCmdFmtImportsOverwrite verifies that --imports --overwrite rewrites both
101 // the root Caddyfile and its imported file.
102 func TestCmdFmtImportsOverwrite(t *testing.T) {
103 dir := t.TempDir()
104
105 rootPath := filepath.Join(dir, "Caddyfile")
106 sitesDir := filepath.Join(dir, "sites")
107 if err := os.MkdirAll(sitesDir, 0o755); err != nil {
108 t.Fatal(err)
109 }
110 importedPath := filepath.Join(sitesDir, "a.caddy")
111
112 // Root imports the sites file; both are deliberately messy.
113 writeTestFile(t, rootPath, "import sites/a.caddy\n")
114 writeTestFile(t, importedPath, "localhost {\nrespond 200\n}\n")
115 if err := os.Chmod(rootPath, 0o640); err != nil {
116 t.Fatal(err)
117 }
118 rootInfoBefore, err := os.Stat(rootPath)
119 if err != nil {
120 t.Fatal(err)
121 }
122 importedInfoBefore, err := os.Stat(importedPath)
123 if err != nil {
124 t.Fatal(err)
125 }
126 // Windows loads FileInfo IDs lazily when SameFile is first called. Prime
127 // them before replacement so these values continue to identify the old files.
128 if !os.SameFile(rootInfoBefore, rootInfoBefore) || !os.SameFile(importedInfoBefore, importedInfoBefore) {
129 t.Fatal("could not snapshot file identity")
130 }
131
132 fl := newFmtFlags(rootPath, true, false, true)
133 code, err := cmdFmt(fl)
134 if err != nil {
135 t.Fatalf("cmdFmt returned error: %v", err)
136 }
137 if code != caddy.ExitCodeSuccess {
138 t.Errorf("expected ExitCodeSuccess, got %d", code)
139 }
140
141 gotRoot, err := os.ReadFile(rootPath)
142 if err != nil {
143 t.Fatal(err)
144 }
145 if want := "import sites/a.caddy\n"; string(gotRoot) != want {
146 t.Errorf("root file content = %q, want %q", string(gotRoot), want)
147 }
148 gotRootInfo, err := os.Stat(rootPath)
149 if err != nil {
150 t.Fatal(err)
151 }
152 if gotRootInfo.Mode().Perm() != rootInfoBefore.Mode().Perm() {
153 t.Errorf("root file permissions = %o, want %o", gotRootInfo.Mode().Perm(), rootInfoBefore.Mode().Perm())
154 }
155 if os.SameFile(rootInfoBefore, gotRootInfo) {
156 t.Error("root file was rewritten in place instead of atomically replaced")
157 }
158
159 got, err := os.ReadFile(importedPath)
160 if err != nil {
161 t.Fatal(err)
162 }
163 want := "localhost {\n\trespond 200\n}\n"
164 if string(got) != want {
165 t.Errorf("imported file content = %q, want %q", string(got), want)
166 }
167 gotImportedInfo, err := os.Stat(importedPath)
168 if err != nil {
169 t.Fatal(err)
170 }
171 if os.SameFile(importedInfoBefore, gotImportedInfo) {
172 t.Error("imported file was rewritten in place instead of atomically replaced")
173 }
174 }
175
176 func TestCmdFmtImportsPreviewReportsFormattingDifference(t *testing.T) {
177 for _, diff := range []bool{false, true} {
178 t.Run(map[bool]string{false: "preview", true: "diff"}[diff], func(t *testing.T) {
179 dir := t.TempDir()
180 rootPath := filepath.Join(dir, "Caddyfile")
181 importedPath := filepath.Join(dir, "imported.caddy")
182 writeTestFile(t, rootPath, "import imported.caddy\n")
183 writeTestFile(t, importedPath, "localhost {\nrespond 200\n}\n")
184
185 output, code, err := captureStdout(t, func() (int, error) {
186 return cmdFmt(newFmtFlags(rootPath, false, diff, true))
187 })
188 if err == nil || !strings.Contains(err.Error(), "input is not formatted") {
189 t.Fatalf("expected formatting error, got %v", err)
190 }
191 if code != caddy.ExitCodeFailedStartup {
192 t.Errorf("expected ExitCodeFailedStartup, got %d", code)
193 }
194 if !strings.Contains(output, "# "+canonicalTestPath(t, importedPath)) {
195 t.Errorf("output missing imported file header; got:\n%s", output)
196 }
197 if diff && !strings.Contains(output, "+ \trespond 200") {
198 t.Errorf("output missing formatted diff; got:\n%s", output)
199 }
200 if !diff && !strings.Contains(output, "localhost {\n\trespond 200\n}") {
201 t.Errorf("output missing formatted preview; got:\n%s", output)
202 }
203 })
204 }
205 }
206
207 func TestOverwriteFormattedFilesRejectsSymlink(t *testing.T) {
208 dir := t.TempDir()
209 targetPath := filepath.Join(dir, "target")
210 linkPath := filepath.Join(dir, "link")
211 writeTestFile(t, targetPath, "original")
212 if err := os.Symlink(targetPath, linkPath); err != nil {
213 t.Skipf("symlinks unavailable: %v", err)
214 }
215
216 err := overwriteFormattedFiles([]caddyfile.FormattedFile{{Path: linkPath, Content: []byte("changed")}})
217 if err == nil || !strings.Contains(err.Error(), "refusing to overwrite") {
218 t.Fatalf("expected symlink rejection, got %v", err)
219 }
220 got, err := os.ReadFile(targetPath)
221 if err != nil {
222 t.Fatal(err)
223 }
224 if !bytes.Equal(got, []byte("original")) {
225 t.Errorf("symlink target changed to %q", got)
226 }
227 }
228
229 func TestOverwriteFormattedFilesReportsPartialUpdate(t *testing.T) {
230 dir := t.TempDir()
231 path := filepath.Join(dir, "Caddyfile")
232 writeTestFile(t, path, "localhost {\nrespond 200\n}\n")
233 formatted := []byte("localhost {\n\trespond 200\n}\n")
234
235 err := overwriteFormattedFiles([]caddyfile.FormattedFile{
236 {Path: path, Content: formatted},
237 {Path: path, Content: formatted},
238 })
239 if err == nil || !strings.Contains(err.Error(), "after replacing 1 of 2 files") ||
240 !strings.Contains(err.Error(), "may be partially updated") {
241 t.Fatalf("expected partial-update error, got %v", err)
242 }
243 got, readErr := os.ReadFile(path)
244 if readErr != nil {
245 t.Fatal(readErr)
246 }
247 if !bytes.Equal(got, formatted) {
248 t.Errorf("first replacement content = %q, want %q", got, formatted)
249 }
250 }
251
252 // TestCmdFmtImportsPrintsHeaders verifies that without --overwrite each file's
253 // output is preceded by a "# <path>" header line.
254 func TestCmdFmtImportsPrintsHeaders(t *testing.T) {
255 dir := t.TempDir()
256
257 rootPath := filepath.Join(dir, "Caddyfile")
258 sitesDir := filepath.Join(dir, "sites")
259 if err := os.MkdirAll(sitesDir, 0o755); err != nil {
260 t.Fatal(err)
261 }
262 importedPath := filepath.Join(sitesDir, "b.caddy")
263
264 writeTestFile(t, rootPath, "import sites/b.caddy\n")
265 writeTestFile(t, importedPath, "localhost {\n\trespond 200\n}\n")
266
267 fl := newFmtFlags(rootPath, false, false, true)
268 output, code, fmtErr := captureStdout(t, func() (int, error) { return cmdFmt(fl) })
269
270 if fmtErr != nil {
271 t.Fatalf("cmdFmt returned error: %v", fmtErr)
272 }
273 if code != caddy.ExitCodeSuccess {
274 t.Errorf("expected ExitCodeSuccess, got %d", code)
275 }
276 if !strings.Contains(output, "# "+canonicalTestPath(t, rootPath)) {
277 t.Errorf("output missing root header; got:\n%s", output)
278 }
279 if !strings.Contains(output, "# "+canonicalTestPath(t, importedPath)) {
280 t.Errorf("output missing imported file header; got:\n%s", output)
281 }
282 }
283
284 func TestCommandsAreAvailable(t *testing.T) {
285 // trigger init, and build the default factory, so that
286 // all commands from this package are available
287 cmd := defaultFactory.Build()
288 if cmd == nil {
289 t.Fatal("default factory failed to build")
290 }
291
292 // check that the default factory has 17 commands; it doesn't
293 // include the commands registered through calls to init in
294 // other packages
295 cmds := Commands()
296 if len(cmds) != 17 {
297 t.Errorf("expected 17 commands, got %d", len(cmds))
298 }
299
300 commandNames := slices.Collect(maps.Keys(cmds))
301 slices.Sort(commandNames)
302
303 expectedCommandNames := []string{
304 "adapt", "add-package", "build-info", "completion",
305 "environ", "fmt", "list-modules", "manpage",
306 "reload", "remove-package", "run", "start",
307 "stop", "storage", "upgrade", "validate", "version",
308 }
309
310 if !reflect.DeepEqual(expectedCommandNames, commandNames) {
311 t.Errorf("expected %v, got %v", expectedCommandNames, commandNames)
312 }
313 }
314
315 func TestCmdRun_UnbufferOnFailure(t *testing.T) {
316 fs := pflag.NewFlagSet("run", pflag.ContinueOnError)
317 fs.StringP("config", "c", "", "")
318 fs.StringP("adapter", "a", "", "")
319 fs.BoolP("resume", "r", false, "")
320 fs.BoolP("environ", "e", false, "")
321 fs.BoolP("watch", "w", false, "")
322 fs.String("pidfile", "", "")
323 fs.String("pingback", "", "")
324 fs.StringSlice("envfile", nil, "")
325 _ = fs.Set("config", filepath.Join(t.TempDir(), "nonexistent"))
326
327 code, err := cmdRun(Flags{fs})
328 if err == nil {
329 t.Fatal("expected error from cmdRun with nonexistent config, got nil")
330 }
331 if code != caddy.ExitCodeFailedStartup {
332 t.Errorf("expected exit code %d, got %d", caddy.ExitCodeFailedStartup, code)
333 }
334
335 // Regression test for #8180: caddy.Log() must not remain buffered after cmdRun failure
336 if _, ok := caddy.Log().Core().(internal.LogBufferCoreInterface); ok {
337 t.Error("caddy.Log() was left in a buffered state after cmdRun failed; should be unbuffered")
338 }
339 }
340
341 func TestWrapCommandFuncForCobra_RunFailure(t *testing.T) {
342 fs := pflag.NewFlagSet("run", pflag.ContinueOnError)
343 fs.StringP("config", "c", "", "")
344 fs.StringP("adapter", "a", "", "")
345 fs.BoolP("resume", "r", false, "")
346 fs.BoolP("environ", "e", false, "")
347 fs.BoolP("watch", "w", false, "")
348 fs.String("pidfile", "", "")
349 fs.String("pingback", "", "")
350 fs.StringSlice("envfile", nil, "")
351 _ = fs.Set("config", filepath.Join(t.TempDir(), "nonexistent"))
352
353 cmd := &cobra.Command{Use: "run"}
354 cmd.Flags().AddFlagSet(fs)
355
356 runE := WrapCommandFuncForCobra(cmdRun)
357 err := runE(cmd, nil)
358 if err == nil {
359 t.Fatal("expected error from wrapped cmdRun, got nil")
360 }
361
362 // Regression test for #8180: caddy.Log() must not remain buffered after WrapCommandFuncForCobra
363 if _, ok := caddy.Log().Core().(internal.LogBufferCoreInterface); ok {
364 t.Error("caddy.Log() was left in a buffered state after WrapCommandFuncForCobra; should be unbuffered")
365 }
366 }