merge v0.23.0-rc changes

This commit is contained in:
Gani Georgiev
2024-09-29 19:23:19 +03:00
parent ad92992324
commit 844f18cac3
753 changed files with 85141 additions and 63396 deletions
+344 -380
View File
@@ -1,422 +1,386 @@
package apis_test
import (
"database/sql"
"errors"
"bytes"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/rest"
"github.com/spf13/cast"
"github.com/pocketbase/pocketbase/tools/router"
)
func Test404(t *testing.T) {
func TestWrapStdHandler(t *testing.T) {
t.Parallel()
scenarios := []tests.ApiScenario{
{
Method: http.MethodGet,
Url: "/api/missing",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Method: http.MethodPost,
Url: "/api/missing",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Method: http.MethodPatch,
Url: "/api/missing",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Method: http.MethodDelete,
Url: "/api/missing",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Method: http.MethodHead,
Url: "/api/missing",
ExpectedStatus: 404,
},
}
app, _ := tests.NewTestApp()
defer app.Cleanup()
for _, scenario := range scenarios {
scenario.Test(t)
}
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
func TestCustomRoutesAndErrorsHandling(t *testing.T) {
t.Parallel()
e := new(core.RequestEvent)
e.App = app
e.Request = req
e.Response = rec
scenarios := []tests.ApiScenario{
{
Name: "custom route",
Method: http.MethodGet,
Url: "/custom",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/custom",
Handler: func(c echo.Context) error {
return c.String(200, "test123")
},
})
},
ExpectedStatus: 200,
ExpectedContent: []string{"test123"},
},
{
Name: "custom route with url encoded parameter",
Method: http.MethodGet,
Url: "/a%2Bb%2Bc",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/:param",
Handler: func(c echo.Context) error {
return c.String(200, c.PathParam("param"))
},
})
},
ExpectedStatus: 200,
ExpectedContent: []string{"a+b+c"},
},
{
Name: "route with HTTPError",
Method: http.MethodGet,
Url: "/http-error",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/http-error",
Handler: func(c echo.Context) error {
return echo.ErrBadRequest
},
})
},
ExpectedStatus: 400,
ExpectedContent: []string{`{"code":400,"message":"Bad Request.","data":{}}`},
},
{
Name: "route with api error",
Method: http.MethodGet,
Url: "/api-error",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api-error",
Handler: func(c echo.Context) error {
return apis.NewApiError(500, "test message", errors.New("internal_test"))
},
})
},
ExpectedStatus: 500,
ExpectedContent: []string{`{"code":500,"message":"Test message.","data":{}}`},
},
{
Name: "route with plain error",
Method: http.MethodGet,
Url: "/plain-error",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/plain-error",
Handler: func(c echo.Context) error {
return errors.New("Test error")
},
})
},
ExpectedStatus: 400,
ExpectedContent: []string{`{"code":400,"message":"Something went wrong while processing your request.","data":{}}`},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestRemoveTrailingSlashMiddleware(t *testing.T) {
t.Parallel()
scenarios := []tests.ApiScenario{
{
Name: "non /api/* route (exact match)",
Method: http.MethodGet,
Url: "/custom",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/custom",
Handler: func(c echo.Context) error {
return c.String(200, "test123")
},
})
},
ExpectedStatus: 200,
ExpectedContent: []string{"test123"},
},
{
Name: "non /api/* route (with trailing slash)",
Method: http.MethodGet,
Url: "/custom/",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/custom",
Handler: func(c echo.Context) error {
return c.String(200, "test123")
},
})
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
},
{
Name: "/api/* route (exact match)",
Method: http.MethodGet,
Url: "/api/custom",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api/custom",
Handler: func(c echo.Context) error {
return c.String(200, "test123")
},
})
},
ExpectedStatus: 200,
ExpectedContent: []string{"test123"},
},
{
Name: "/api/* route (with trailing slash)",
Method: http.MethodGet,
Url: "/api/custom/",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api/custom",
Handler: func(c echo.Context) error {
return c.String(200, "test123")
},
})
},
ExpectedStatus: 200,
ExpectedContent: []string{"test123"},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestMultiBinder(t *testing.T) {
t.Parallel()
rawJson := `{"name":"test123"}`
formData, mp, err := tests.MockMultipartData(map[string]string{
rest.MultipartJsonKey: rawJson,
})
err := apis.WrapStdHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("test"))
}))(e)
if err != nil {
t.Fatal(err)
}
scenarios := []tests.ApiScenario{
{
Name: "non-api group route",
Method: "POST",
Url: "/custom",
Body: strings.NewReader(rawJson),
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: "POST",
Path: "/custom",
Handler: func(c echo.Context) error {
data := &struct {
Name string `json:"name"`
}{}
if err := c.Bind(data); err != nil {
return err
}
// try to read the body again
r := apis.RequestInfo(c)
if v := cast.ToString(r.Data["name"]); v != "test123" {
t.Fatalf("Expected request data with name %q, got, %q", "test123", v)
}
return c.NoContent(200)
},
})
},
ExpectedStatus: 200,
},
{
Name: "api group route",
Method: "GET",
Url: "/api/admins",
Body: strings.NewReader(rawJson),
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// it is not important whether the route handler return an error since
// we just need to ensure that the eagerRequestInfoCache was registered
next(c)
// ensure that the body was read at least once
data := &struct {
Name string `json:"name"`
}{}
c.Bind(data)
// try to read the body again
r := apis.RequestInfo(c)
if v := cast.ToString(r.Data["name"]); v != "test123" {
t.Fatalf("Expected request data with name %q, got, %q", "test123", v)
}
return nil
}
})
},
ExpectedStatus: 200,
},
{
Name: "custom route with @jsonPayload as multipart body",
Method: "POST",
Url: "/custom",
Body: formData,
RequestHeaders: map[string]string{
"Content-Type": mp.FormDataContentType(),
},
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.AddRoute(echo.Route{
Method: "POST",
Path: "/custom",
Handler: func(c echo.Context) error {
data := &struct {
Name string `json:"name"`
}{}
if err := c.Bind(data); err != nil {
return err
}
// try to read the body again
r := apis.RequestInfo(c)
if v := cast.ToString(r.Data["name"]); v != "test123" {
t.Fatalf("Expected request data with name %q, got, %q", "test123", v)
}
return c.NoContent(200)
},
})
},
ExpectedStatus: 200,
},
}
for _, scenario := range scenarios {
scenario.Test(t)
if body := rec.Body.String(); body != "test" {
t.Fatalf("Expected body %q, got %q", "test", body)
}
}
func TestErrorHandler(t *testing.T) {
func TestWrapStdMiddleware(t *testing.T) {
t.Parallel()
scenarios := []tests.ApiScenario{
app, _ := tests.NewTestApp()
defer app.Cleanup()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e := new(core.RequestEvent)
e.App = app
e.Request = req
e.Response = rec
err := apis.WrapStdMiddleware(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("test"))
})
})(e)
if err != nil {
t.Fatal(err)
}
if body := rec.Body.String(); body != "test" {
t.Fatalf("Expected body %q, got %q", "test", body)
}
}
func TestStatic(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
dir := createTestDir(t)
defer os.RemoveAll(dir)
fsys := os.DirFS(filepath.Join(dir, "sub"))
type staticScenario struct {
path string
indexFallback bool
expectedStatus int
expectBody string
expectError bool
}
scenarios := []staticScenario{
{
Name: "apis.ApiError",
Method: http.MethodGet,
Url: "/test",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.GET("/test", func(c echo.Context) error {
return apis.NewApiError(418, "test", nil)
})
},
ExpectedStatus: 418,
ExpectedContent: []string{`"message":"Test."`},
path: "",
indexFallback: false,
expectedStatus: 200,
expectBody: "sub index.html",
expectError: false,
},
{
Name: "wrapped apis.ApiError",
Method: http.MethodGet,
Url: "/test",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.GET("/test", func(c echo.Context) error {
return fmt.Errorf("example 123: %w", apis.NewApiError(418, "test", nil))
})
},
ExpectedStatus: 418,
ExpectedContent: []string{`"message":"Test."`},
NotExpectedContent: []string{"example", "123"},
path: "missing/a/b/c",
indexFallback: false,
expectedStatus: 404,
expectBody: "",
expectError: true,
},
{
Name: "echo.HTTPError",
Method: http.MethodGet,
Url: "/test",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.GET("/test", func(c echo.Context) error {
return echo.NewHTTPError(418, "test")
})
},
ExpectedStatus: 418,
ExpectedContent: []string{`"message":"Test."`},
path: "missing/a/b/c",
indexFallback: true,
expectedStatus: 200,
expectBody: "sub index.html",
expectError: false,
},
{
Name: "wrapped echo.HTTPError",
Method: http.MethodGet,
Url: "/test",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.GET("/test", func(c echo.Context) error {
return fmt.Errorf("example 123: %w", echo.NewHTTPError(418, "test"))
})
},
ExpectedStatus: 418,
ExpectedContent: []string{`"message":"Test."`},
NotExpectedContent: []string{"example", "123"},
path: "testroot", // parent directory file
indexFallback: false,
expectedStatus: 404,
expectBody: "",
expectError: true,
},
{
Name: "wrapped sql.ErrNoRows",
Method: http.MethodGet,
Url: "/test",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.GET("/test", func(c echo.Context) error {
return fmt.Errorf("example 123: %w", sql.ErrNoRows)
})
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
NotExpectedContent: []string{"example", "123"},
path: "test",
indexFallback: false,
expectedStatus: 200,
expectBody: "sub test",
expectError: false,
},
{
Name: "custom error",
Method: http.MethodGet,
Url: "/test",
BeforeTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
e.GET("/test", func(c echo.Context) error {
return fmt.Errorf("example 123")
})
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
NotExpectedContent: []string{"example", "123"},
path: "sub2",
indexFallback: false,
expectedStatus: 301,
expectBody: "",
expectError: false,
},
{
path: "sub2/",
indexFallback: false,
expectedStatus: 200,
expectBody: "sub2 index.html",
expectError: false,
},
{
path: "sub2/test",
indexFallback: false,
expectedStatus: 200,
expectBody: "sub2 test",
expectError: false,
},
{
path: "sub2/test/",
indexFallback: false,
expectedStatus: 301,
expectBody: "",
expectError: false,
},
}
for _, scenario := range scenarios {
scenario.Test(t)
// extra directory traversal checks
dtp := []string{
"/../",
"\\../",
"../",
"../../",
"..\\",
"..\\..\\",
"../..\\",
"..\\..//",
`%2e%2e%2f`,
`%2e%2e%2f%2e%2e%2f`,
`%2e%2e/`,
`%2e%2e/%2e%2e/`,
`..%2f`,
`..%2f..%2f`,
`%2e%2e%5c`,
`%2e%2e%5c%2e%2e%5c`,
`%2e%2e\`,
`%2e%2e\%2e%2e\`,
`..%5c`,
`..%5c..%5c`,
`%252e%252e%255c`,
`%252e%252e%255c%252e%252e%255c`,
`..%255c`,
`..%255c..%255c`,
}
for _, p := range dtp {
scenarios = append(scenarios,
staticScenario{
path: p + "testroot",
indexFallback: false,
expectedStatus: 404,
expectBody: "",
expectError: true,
},
staticScenario{
path: p + "testroot",
indexFallback: true,
expectedStatus: 200,
expectBody: "sub index.html",
expectError: false,
},
)
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s_%v", i, s.path, s.indexFallback), func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/"+s.path, nil)
req.SetPathValue(apis.StaticWildcardParam, s.path)
rec := httptest.NewRecorder()
e := new(core.RequestEvent)
e.App = app
e.Request = req
e.Response = rec
err := apis.Static(fsys, s.indexFallback)(e)
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
}
body := rec.Body.String()
if body != s.expectBody {
t.Fatalf("Expected body %q, got %q", s.expectBody, body)
}
if hasErr {
apiErr := router.ToApiError(err)
if apiErr.Status != s.expectedStatus {
t.Fatalf("Expected status code %d, got %d", s.expectedStatus, apiErr.Status)
}
}
})
}
}
func TestFindUploadedFiles(t *testing.T) {
scenarios := []struct {
filename string
expectedPattern string
}{
{"ab.png", `^ab\w{10}_\w{10}\.png$`},
{"test", `^test_\w{10}\.txt$`},
{"a b c d!@$.j!@$pg", `^a_b_c_d_\w{10}\.jpg$`},
{strings.Repeat("a", 150), `^a{100}_\w{10}\.txt$`},
}
for _, s := range scenarios {
t.Run(s.filename, func(t *testing.T) {
// create multipart form file body
body := new(bytes.Buffer)
mp := multipart.NewWriter(body)
w, err := mp.CreateFormFile("test", s.filename)
if err != nil {
t.Fatal(err)
}
w.Write([]byte("test"))
mp.Close()
// ---
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Add("Content-Type", mp.FormDataContentType())
result, err := apis.FindUploadedFiles(req, "test")
if err != nil {
t.Fatal(err)
}
if len(result) != 1 {
t.Fatalf("Expected 1 file, got %d", len(result))
}
if result[0].Size != 4 {
t.Fatalf("Expected the file size to be 4 bytes, got %d", result[0].Size)
}
pattern, err := regexp.Compile(s.expectedPattern)
if err != nil {
t.Fatalf("Invalid filename pattern %q: %v", s.expectedPattern, err)
}
if !pattern.MatchString(result[0].Name) {
t.Fatalf("Expected filename to match %s, got filename %s", s.expectedPattern, result[0].Name)
}
})
}
}
func TestFindUploadedFilesMissing(t *testing.T) {
body := new(bytes.Buffer)
mp := multipart.NewWriter(body)
mp.Close()
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Add("Content-Type", mp.FormDataContentType())
result, err := apis.FindUploadedFiles(req, "test")
if err == nil {
t.Error("Expected error, got nil")
}
if result != nil {
t.Errorf("Expected result to be nil, got %v", result)
}
}
func TestMustSubFS(t *testing.T) {
t.Parallel()
dir := createTestDir(t)
defer os.RemoveAll(dir)
// invalid path (no beginning and ending slashes)
if !hasPanicked(func() {
apis.MustSubFS(os.DirFS(dir), "/test/")
}) {
t.Fatalf("Expected to panic")
}
// valid path
if hasPanicked(func() {
apis.MustSubFS(os.DirFS(dir), "./////a/b/c") // checks if ToSlash was called
}) {
t.Fatalf("Didn't expect to panic")
}
// check sub content
sub := apis.MustSubFS(os.DirFS(dir), "sub")
_, err := sub.Open("test")
if err != nil {
t.Fatalf("Missing expected file sub/test")
}
}
// -------------------------------------------------------------------
func hasPanicked(f func()) (didPanic bool) {
defer func() {
if r := recover(); r != nil {
didPanic = true
}
}()
f()
return
}
// note: make sure to call os.RemoveAll(dir) after you are done
// working with the created test dir.
func createTestDir(t *testing.T) string {
dir, err := os.MkdirTemp(os.TempDir(), "test_dir")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("root index.html"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "testroot"), []byte("root test"), 0644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(dir, "sub"), os.ModePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "sub/index.html"), []byte("sub index.html"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "sub/test"), []byte("sub test"), 0644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(dir, "sub", "sub2"), os.ModePerm); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "sub/sub2/index.html"), []byte("sub2 index.html"), 0644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "sub/sub2/test"), []byte("sub2 test"), 0644); err != nil {
t.Fatal(err)
}
return dir
}