diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 06c73988..21034b83 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -34,6 +34,9 @@ jobs: # - name: Run linter # uses: golangci/golangci-lint-action@v3 + - name: Run tests + run: go test ./... + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v3 with: diff --git a/apis/admin_test.go b/apis/admin_test.go index 7a1c7d08..6ebcb8b0 100644 --- a/apis/admin_test.go +++ b/apis/admin_test.go @@ -7,8 +7,10 @@ import ( "github.com/labstack/echo/v5" "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/models" "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" ) func TestAdminAuth(t *testing.T) { @@ -100,14 +102,12 @@ func TestAdminRequestPasswordReset(t *testing.T) { Url: "/api/admins/request-password-reset", Body: strings.NewReader(`{"email":"test@example.com"}`), ExpectedStatus: 204, - // usually this events are fired but since the submit is - // executed in a separate go routine they are fired async - // ExpectedEvents: map[string]int{ - // "OnModelBeforeUpdate": 1, - // "OnModelAfterUpdate": 1, - // "OnMailerBeforeUserResetPasswordSend:1": 1, - // "OnMailerAfterUserResetPasswordSend:1": 1, - // }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnMailerBeforeAdminResetPasswordSend": 1, + "OnMailerAfterAdminResetPasswordSend": 1, + }, }, { Name: "existing admin (after already sent)", @@ -115,6 +115,18 @@ func TestAdminRequestPasswordReset(t *testing.T) { Url: "/api/admins/request-password-reset", Body: strings.NewReader(`{"email":"test@example.com"}`), ExpectedStatus: 204, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + // simulate recent password request + admin, err := app.Dao().FindAdminByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + admin.LastResetSentAt = types.NowDateTime() + dao := daos.New(app.Dao().DB()) // new dao to ignore hooks + if err := dao.Save(admin); err != nil { + t.Fatal(err) + } + }, }, } diff --git a/apis/user_test.go b/apis/user_test.go index b42527a7..f382236f 100644 --- a/apis/user_test.go +++ b/apis/user_test.go @@ -6,7 +6,9 @@ import ( "testing" "github.com/labstack/echo/v5" + "github.com/pocketbase/pocketbase/daos" "github.com/pocketbase/pocketbase/tests" + "github.com/pocketbase/pocketbase/tools/types" ) func TestUsersAuthMethods(t *testing.T) { @@ -140,14 +142,12 @@ func TestUserRequestPasswordReset(t *testing.T) { Url: "/api/users/request-password-reset", Body: strings.NewReader(`{"email":"test@example.com"}`), ExpectedStatus: 204, - // usually this events are fired but since the submit is - // executed in a separate go routine they are fired async - // ExpectedEvents: map[string]int{ - // "OnModelBeforeUpdate": 1, - // "OnModelAfterUpdate": 1, - // "OnMailerBeforeUserResetPasswordSend": 1, - // "OnMailerAfterUserResetPasswordSend": 1, - // }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnMailerBeforeUserResetPasswordSend": 1, + "OnMailerAfterUserResetPasswordSend": 1, + }, }, { Name: "existing user (after already sent)", @@ -155,6 +155,18 @@ func TestUserRequestPasswordReset(t *testing.T) { Url: "/api/users/request-password-reset", Body: strings.NewReader(`{"email":"test@example.com"}`), ExpectedStatus: 204, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + // simulate recent password request + user, err := app.Dao().FindUserByEmail("test@example.com") + if err != nil { + t.Fatal(err) + } + user.LastResetSentAt = types.NowDateTime() + dao := daos.New(app.Dao().DB()) // new dao to ignore hooks + if err := dao.Save(user); err != nil { + t.Fatal(err) + } + }, }, } @@ -216,50 +228,67 @@ func TestUserConfirmPasswordReset(t *testing.T) { func TestUserRequestVerification(t *testing.T) { scenarios := []tests.ApiScenario{ - // empty data { + Name: "empty data", Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(``), ExpectedStatus: 400, ExpectedContent: []string{`"data":{"email":{"code":"validation_required","message":"Cannot be blank."}}`}, }, - // invalid data { + Name: "invalid data", Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(`{"email`), ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, }, - // missing user { + Name: "missing user", Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(`{"email":"missing@example.com"}`), ExpectedStatus: 204, }, - // existing already verified user { + Name: "existing already verified user", Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(`{"email":"test@example.com"}`), ExpectedStatus: 204, }, - // existing unverified user { + Name: "existing unverified user", Method: http.MethodPost, Url: "/api/users/request-verification", Body: strings.NewReader(`{"email":"test2@example.com"}`), ExpectedStatus: 204, - // usually this events are fired but since the submit is - // executed in a separate go routine they are fired async - // ExpectedEvents: map[string]int{ - // "OnModelBeforeUpdate": 1, - // "OnModelAfterUpdate": 1, - // "OnMailerBeforeUserVerificationSend": 1, - // "OnMailerAfterUserVerificationSend": 1, - // }, + ExpectedEvents: map[string]int{ + "OnModelBeforeUpdate": 1, + "OnModelAfterUpdate": 1, + "OnMailerBeforeUserVerificationSend": 1, + "OnMailerAfterUserVerificationSend": 1, + }, + }, + { + Name: "existing unverified user (after already sent)", + Method: http.MethodPost, + Url: "/api/users/request-verification", + Body: strings.NewReader(`{"email":"test2@example.com"}`), + ExpectedStatus: 204, + BeforeFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) { + // simulate recent verification sent + user, err := app.Dao().FindUserByEmail("test2@example.com") + if err != nil { + t.Fatal(err) + } + user.LastVerificationSentAt = types.NowDateTime() + dao := daos.New(app.Dao().DB()) // new dao to ignore hooks + if err := dao.Save(user); err != nil { + t.Fatal(err) + } + }, }, } diff --git a/daos/record_expand_test.go b/daos/record_expand_test.go index f64474b2..86dd9cf5 100644 --- a/daos/record_expand_test.go +++ b/daos/record_expand_test.go @@ -23,7 +23,7 @@ func TestExpandRecords(t *testing.T) { expands []string fetchFunc daos.ExpandFetchFunc expectExpandProps int - expectExpandFailires int + expectExpandFailures int }{ // empty records { @@ -130,8 +130,8 @@ func TestExpandRecords(t *testing.T) { records, _ := app.Dao().FindRecordsByIds(col, ids, nil) failed := app.Dao().ExpandRecords(records, s.expands, s.fetchFunc) - if len(failed) != s.expectExpandFailires { - t.Errorf("(%d) Expected %d failures, got %d: \n%v", i, s.expectExpandFailires, len(failed), failed) + if len(failed) != s.expectExpandFailures { + t.Errorf("(%d) Expected %d failures, got %d: \n%v", i, s.expectExpandFailures, len(failed), failed) } encoded, _ := json.Marshal(records) @@ -155,7 +155,7 @@ func TestExpandRecord(t *testing.T) { expands []string fetchFunc daos.ExpandFetchFunc expectExpandProps int - expectExpandFailires int + expectExpandFailures int }{ // empty expand { @@ -241,8 +241,8 @@ func TestExpandRecord(t *testing.T) { record, _ := app.Dao().FindFirstRecordByData(col, "id", s.recordId) failed := app.Dao().ExpandRecord(record, s.expands, s.fetchFunc) - if len(failed) != s.expectExpandFailires { - t.Errorf("(%d) Expected %d failures, got %d: \n%v", i, s.expectExpandFailires, len(failed), failed) + if len(failed) != s.expectExpandFailures { + t.Errorf("(%d) Expected %d failures, got %d: \n%v", i, s.expectExpandFailures, len(failed), failed) } encoded, _ := json.Marshal(record) diff --git a/tests/api.go b/tests/api.go index 42fc05f7..66c467d2 100644 --- a/tests/api.go +++ b/tests/api.go @@ -27,7 +27,7 @@ type ApiScenario struct { ExpectedContent []string NotExpectedContent []string ExpectedEvents map[string]int - // test events + // test hooks BeforeFunc func(t *testing.T, app *TestApp, e *echo.Echo) AfterFunc func(t *testing.T, app *TestApp, e *echo.Echo) } @@ -81,6 +81,12 @@ func (scenario *ApiScenario) Test(t *testing.T) { t.Errorf("[%s] Expected status code %d, got %d", prefix, scenario.ExpectedStatus, res.StatusCode) } + // @todo consider replacing with sync.WaitGroup + // + // apply a small delay before checking the expectations to ensure + // that all fired go routines have complicated before cleaning up the app instance + time.Sleep(5 * time.Millisecond) + if len(scenario.ExpectedContent) == 0 && len(scenario.NotExpectedContent) == 0 { if len(recorder.Body.Bytes()) != 0 { t.Errorf("[%s] Expected empty body, got \n%v", prefix, recorder.Body.String())