| package tests
|
|
|
| import (
|
| "bytes"
|
| "context"
|
| "encoding/json"
|
| "fmt"
|
| "io"
|
| "maps"
|
| "net/http"
|
| "net/http/httptest"
|
| "strings"
|
| "testing"
|
| "time"
|
|
|
| "github.com/pocketbase/pocketbase/apis"
|
| "github.com/pocketbase/pocketbase/core"
|
| pbtests "github.com/pocketbase/pocketbase/tests"
|
| "github.com/pocketbase/pocketbase/tools/hook"
|
| )
|
|
|
|
|
|
|
|
|
|
|
|
|
| type ApiScenario struct {
|
|
|
| Name string
|
|
|
|
|
| Method string
|
|
|
|
|
| URL string
|
|
|
|
|
|
|
|
|
|
|
|
|
| Body io.Reader
|
|
|
|
|
| Headers map[string]string
|
|
|
|
|
|
|
| Delay time.Duration
|
|
|
|
|
|
|
|
|
| Timeout time.Duration
|
|
|
|
|
|
|
|
|
|
|
| ExpectedStatus int
|
|
|
|
|
|
|
|
|
|
|
| ExpectedContent []string
|
|
|
|
|
|
|
|
|
|
|
| NotExpectedContent []string
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ExpectedEvents map[string]int
|
|
|
|
|
|
|
|
|
| TestAppFactory func(t testing.TB) *pbtests.TestApp
|
| BeforeTestFunc func(t testing.TB, app *pbtests.TestApp, e *core.ServeEvent)
|
| AfterTestFunc func(t testing.TB, app *pbtests.TestApp, res *http.Response)
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| func (scenario *ApiScenario) Test(t *testing.T) {
|
| t.Run(scenario.normalizedName(), func(t *testing.T) {
|
| scenario.test(t)
|
| })
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| func (scenario *ApiScenario) Benchmark(b *testing.B) {
|
| b.Run(scenario.normalizedName(), func(b *testing.B) {
|
| for i := 0; i < b.N; i++ {
|
| scenario.test(b)
|
| }
|
| })
|
| }
|
|
|
| func (scenario *ApiScenario) normalizedName() string {
|
| var name = scenario.Name
|
|
|
| if name == "" {
|
| name = fmt.Sprintf("%s:%s", scenario.Method, scenario.URL)
|
| }
|
|
|
| return name
|
| }
|
|
|
| func (scenario *ApiScenario) test(t testing.TB) {
|
| var testApp *pbtests.TestApp
|
| if scenario.TestAppFactory != nil {
|
| testApp = scenario.TestAppFactory(t)
|
| if testApp == nil {
|
| t.Fatal("TestAppFactory must return a non-nill app instance")
|
| }
|
| } else {
|
| var testAppErr error
|
| testApp, testAppErr = pbtests.NewTestApp()
|
| if testAppErr != nil {
|
| t.Fatalf("Failed to initialize the test app instance: %v", testAppErr)
|
| }
|
| }
|
|
|
|
|
| baseRouter, err := apis.NewRouter(testApp)
|
| if err != nil {
|
| t.Fatal(err)
|
| }
|
|
|
|
|
| serveEvent := new(core.ServeEvent)
|
| serveEvent.App = testApp
|
| serveEvent.Router = baseRouter
|
|
|
| serveErr := testApp.OnServe().Trigger(serveEvent, func(e *core.ServeEvent) error {
|
| if scenario.BeforeTestFunc != nil {
|
| scenario.BeforeTestFunc(t, testApp, e)
|
| }
|
|
|
|
|
| testApp.ResetEventCalls()
|
|
|
|
|
| e.Router.Bind(&hook.Handler[*core.RequestEvent]{
|
| Func: func(re *core.RequestEvent) error {
|
| slowTimer := time.AfterFunc(3*time.Second, func() {
|
| t.Logf("[WARN] Long running test %q", scenario.Name)
|
| })
|
| defer slowTimer.Stop()
|
|
|
| if scenario.Timeout > 0 {
|
| ctx, cancelFunc := context.WithTimeout(re.Request.Context(), scenario.Timeout)
|
| defer cancelFunc()
|
| re.Request = re.Request.Clone(ctx)
|
| }
|
|
|
| return re.Next()
|
| },
|
| Priority: -9999,
|
| })
|
|
|
| recorder := httptest.NewRecorder()
|
|
|
| req := httptest.NewRequest(scenario.Method, scenario.URL, scenario.Body)
|
|
|
|
|
| req.Header.Set("content-type", "application/json")
|
|
|
|
|
| for k, v := range scenario.Headers {
|
| req.Header.Set(k, v)
|
| }
|
|
|
|
|
| mux, err := e.Router.BuildMux()
|
| if err != nil {
|
| t.Fatalf("Failed to build router mux: %v", err)
|
| }
|
| mux.ServeHTTP(recorder, req)
|
|
|
| res := recorder.Result()
|
|
|
| if res.StatusCode != scenario.ExpectedStatus {
|
| t.Errorf("Expected status code %d, got %d", scenario.ExpectedStatus, res.StatusCode)
|
| }
|
|
|
| if scenario.Delay > 0 {
|
| time.Sleep(scenario.Delay)
|
| }
|
|
|
| if len(scenario.ExpectedContent) == 0 && len(scenario.NotExpectedContent) == 0 {
|
| if len(recorder.Body.Bytes()) != 0 {
|
| t.Errorf("Expected empty body, got \n%v", recorder.Body.String())
|
| }
|
| } else {
|
|
|
| buffer := new(bytes.Buffer)
|
| err := json.Compact(buffer, recorder.Body.Bytes())
|
| var normalizedBody string
|
| if err != nil {
|
|
|
| normalizedBody = recorder.Body.String()
|
| } else {
|
| normalizedBody = buffer.String()
|
| }
|
|
|
| for _, item := range scenario.ExpectedContent {
|
| if !strings.Contains(normalizedBody, item) {
|
| t.Errorf("Cannot find %v in response body \n%v", item, normalizedBody)
|
| break
|
| }
|
| }
|
|
|
| for _, item := range scenario.NotExpectedContent {
|
| if strings.Contains(normalizedBody, item) {
|
| t.Errorf("Didn't expect %v in response body \n%v", item, normalizedBody)
|
| break
|
| }
|
| }
|
| }
|
|
|
| remainingEvents := maps.Clone(testApp.EventCalls)
|
|
|
| var noOtherEventsShouldRemain bool
|
| for event, expectedNum := range scenario.ExpectedEvents {
|
| if event == "*" && expectedNum <= 0 {
|
| noOtherEventsShouldRemain = true
|
| continue
|
| }
|
|
|
| actualNum := remainingEvents[event]
|
| if actualNum != expectedNum {
|
| t.Errorf("Expected event %s to be called %d, got %d", event, expectedNum, actualNum)
|
| }
|
|
|
| delete(remainingEvents, event)
|
| }
|
|
|
| if noOtherEventsShouldRemain && len(remainingEvents) > 0 {
|
| t.Errorf("Missing expected remaining events:\n%#v\nAll triggered app events are:\n%#v", remainingEvents, testApp.EventCalls)
|
| }
|
|
|
| if scenario.AfterTestFunc != nil {
|
| scenario.AfterTestFunc(t, testApp, res)
|
| }
|
|
|
| return nil
|
| })
|
| if serveErr != nil {
|
| t.Fatalf("Failed to trigger app serve hook: %v", serveErr)
|
| }
|
| }
|
|
|