keiji-ctl_test.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "net/http"
  6. "net/http/httptest"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "strings"
  11. "testing"
  12. "git.aetherial.dev/aeth/keiji/pkg/controller"
  13. "github.com/gin-gonic/gin"
  14. )
  15. func TestNewRequestOrigin(t *testing.T) {
  16. for _, tc := range []struct{ target, origin string }{
  17. {"https://example.com:8443/a?x=1#fragment", "https://example.com:8443"},
  18. {"http://[::1]:8080/path", "http://[::1]:8080"},
  19. } {
  20. req, err := newRequest(http.MethodPost, tc.target, nil)
  21. if err != nil || req.Header.Get("Origin") != tc.origin {
  22. t.Errorf("%s: request=%v err=%v", tc.target, req, err)
  23. }
  24. }
  25. for _, target := range []string{"/relative", "ftp://example.com/path", "https://user:pass@example.com", "http:///path", "://invalid"} {
  26. if _, err := newRequest(http.MethodPost, target, nil); err == nil {
  27. t.Errorf("accepted invalid target %q", target)
  28. }
  29. }
  30. req, err := newRequest(http.MethodGet, "https://example.com", nil)
  31. if err != nil || req.Header.Get("Origin") != "" {
  32. t.Fatalf("GET: request=%v err=%v", req, err)
  33. }
  34. if _, err := newfileUploadRequest("://invalid", nil, "file", createFile(t)); err == nil {
  35. t.Fatal("upload accepted invalid target")
  36. }
  37. }
  38. func createFile(t *testing.T) string {
  39. t.Helper()
  40. file := filepath.Join(t.TempDir(), "image.png")
  41. if err := os.WriteFile(file, []byte("image"), 0600); err != nil {
  42. t.Fatal(err)
  43. }
  44. return file
  45. }
  46. // Run the real CLI in a subprocess because several command paths call os.Exit.
  47. func TestCLIHelper(t *testing.T) {
  48. if os.Getenv("KEIJI_CTL_TEST_HELPER") != "1" {
  49. return
  50. }
  51. for i, arg := range os.Args {
  52. if arg == "--" {
  53. os.Args = append([]string{"keiji-ctl"}, os.Args[i+1:]...)
  54. flag.CommandLine = flag.NewFlagSet("keiji-ctl", flag.ExitOnError)
  55. main()
  56. return
  57. }
  58. }
  59. t.Fatal("missing helper arguments")
  60. }
  61. func TestCLIWithSameOrigin(t *testing.T) {
  62. t.Setenv("KEIJI_INSECURE_COOKIE", "true")
  63. gin.SetMode(gin.TestMode)
  64. r := gin.New()
  65. r.POST("/login", controller.SameOrigin, func(c *gin.Context) {
  66. http.SetCookie(c.Writer, &http.Cookie{Name: controller.AUTH_COOKIE_NAME, Value: "test-cookie"})
  67. c.Status(http.StatusOK)
  68. })
  69. for _, endpoint := range []string{"asset", "navbar", "menu", "panel"} {
  70. r.POST("/admin/"+endpoint, controller.SameOrigin, func(c *gin.Context) {
  71. cookie, err := c.Request.Cookie(controller.AUTH_COOKIE_NAME)
  72. if err != nil || cookie.Value != "test-cookie" {
  73. c.Status(http.StatusUnauthorized)
  74. return
  75. }
  76. c.Status(http.StatusOK)
  77. })
  78. }
  79. server := httptest.NewServer(r)
  80. defer server.Close()
  81. file := createFile(t)
  82. for _, command := range []string{"auth", "asset", "nav", "menu", "admin"} {
  83. t.Run(command, func(t *testing.T) {
  84. args := []string{"-test.run=^TestCLIHelper$", "--", "-cmd", command, "-address", server.URL, "-cookie", "test-cookie", "-png", file}
  85. cmd := exec.Command(os.Args[0], args...)
  86. cmd.Env = append(os.Environ(), "KEIJI_CTL_TEST_HELPER=1")
  87. output, err := cmd.CombinedOutput()
  88. if err != nil {
  89. t.Fatalf("CLI failed: %v: %s", err, output)
  90. }
  91. if command == "auth" && !strings.Contains(string(output), "test-cookie") {
  92. t.Fatalf("missing auth cookie: %s", output)
  93. }
  94. })
  95. }
  96. }
  97. func TestRedirectNotFollowed(t *testing.T) {
  98. for _, code := range []int{http.StatusFound, http.StatusTemporaryRedirect, http.StatusPermanentRedirect} {
  99. t.Run(fmt.Sprint(code), func(t *testing.T) {
  100. called := false
  101. destination := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true }))
  102. defer destination.Close()
  103. source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  104. http.Redirect(w, r, destination.URL, code)
  105. }))
  106. defer source.Close()
  107. req, err := newRequest(http.MethodPost, source.URL+"/login", strings.NewReader("credentials"))
  108. if err != nil {
  109. t.Fatal(err)
  110. }
  111. req.AddCookie(&http.Cookie{Name: "session", Value: "secret"})
  112. resp, err := newHTTPClient().Do(req)
  113. if err != nil {
  114. t.Fatal(err)
  115. }
  116. defer resp.Body.Close()
  117. if resp.StatusCode != code || called {
  118. t.Fatalf("status=%d, destination called=%v", resp.StatusCode, called)
  119. }
  120. })
  121. }
  122. }