Files
base/apis/record_crud_test.go
zeekay 3e7b0ce670 base: route convention /api/* -> /v1/*
One path: /v1/*. We do not use /api/ — that's Casdoor's path. The
BASE_API_PREFIX env knob is gone; the prefix is hard-coded in
apis/base.go and apis/serve.go banner.

Server-side mounts:
- apis: bindHealthApi, bindCollectionApi, bindRecordCrudApi, etc. all
  attach under r.Group("/v1") (was r.Group(BASE_API_PREFIX, default /api))
- platform plugin: /api/platform/* -> /v1/platform/* (including auth and
  org sub-groups)
- kms plugin: /api/kms/* -> /v1/kms/*
- functions plugin: /api/functions/* -> /v1/functions/*
- cloudsql plugin: /api/cloud-sql/*, /api/meta/* -> /v1/*
- superuser OIDC: /_/api/oidc/config -> /_/v1/oidc/config
- core settings: default RateLimitRule labels /api/batch, /api/ ->
  /v1/batch, /v1/
- apis/batch.go: ValidBatchActions regexes match /v1/collections/*;
  internal rewrite targets /v1/collections/* (was /v1/base/collections/*,
  a stale artifact)

Clients:
- sdk/base-js (BaseClient.collections, files, realtime, CRDT WS,
  signIn/refresh/health) targets /v1/*
- sdk/base-react (use-crdt WS path) targets /v1/crdt
- ui-react admin (lib/api.ts, settings.application copy) targets /v1/*
- cmd/cli (NewClient.APIPrefix) defaults to /v1; client_test.go and
  cmd/cli_test.go stubServer use /v1/*
- cmd/typegen calls /v1/collections

Upstream paths untouched: Casdoor IAM (/api/login, /api/userinfo,
/api/signup, /api/send-verification-code, /api/get-user), KMS Infisical
(/api/v1/auth/universal-auth/login, /api/v2/workspace/environments),
Kubernetes tasks (/api/v1/namespaces/...), IPFS (/api/v0/...), Gitea/
GitLab/Discord/Strava/Patreon/Box/Wakatime upstream APIs. Those are
their contracts, not ours.

Tests: all 437 prior /api/ test URLs across apis/*_test.go and
plugins/platform/auth_proxy_test.go updated to /v1/. Build + go vet
clean. ./apis ./core ./plugins/platform ./plugins/jsvm ./forms all
green. Pre-existing red probes (network/TestRedProbe_*,
migratecmd/TestAutomigrateCollection*) unchanged.
2026-05-15 14:41:26 -07:00

3356 lines
112 KiB
Go

package apis_test
import (
"bytes"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/hanzoai/base/apis"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/tests"
"github.com/hanzoai/base/tools/router"
"github.com/hanzoai/base/tools/types"
)
func TestRecordCrudList(t *testing.T) {
t.Parallel()
scenarios := []tests.ApiScenario{
{
Name: "missing collection",
Method: http.MethodGet,
URL: "/v1/collections/missing/records",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "unauthenticated trying to access nil rule collection (aka. need superuser auth)",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records",
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "authenticated record trying to access nil rule collection (aka. need superuser auth)",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "public collection but with superuser only filter param (aka. @collection, @request, etc.)",
Method: http.MethodGet,
URL: "/v1/collections/demo2/records?filter=%40collection.demo2.title='test1'",
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "public collection but with superuser only sort param (aka. @collection, @request, etc.)",
Method: http.MethodGet,
URL: "/v1/collections/demo2/[email protected]",
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "public collection but with ENCODED superuser only filter/sort (aka. @collection)",
Method: http.MethodGet,
URL: "/v1/collections/demo2/records?filter=%40collection.demo2.title%3D%27test1%27",
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "public collection",
Method: http.MethodGet,
URL: "/v1/collections/demo2/records",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":3`,
`"items":[{`,
`"id":"0yxhwia2amd8gec"`,
`"id":"achvryl401bhse3"`,
`"id":"llvuca81nly1qls"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 3,
},
},
{
Name: "public collection (using the collection id)",
Method: http.MethodGet,
URL: "/v1/collections/sz5l5z67tg7gku0/records",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":3`,
`"items":[{`,
`"id":"0yxhwia2amd8gec"`,
`"id":"achvryl401bhse3"`,
`"id":"llvuca81nly1qls"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 3,
},
},
{
Name: "authorized as superuser trying to access nil rule collection (aka. need superuser auth)",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":3`,
`"items":[{`,
`"id":"al1h9ijdeojtsjy"`,
`"id":"84nmscqy84lsi1t"`,
`"id":"imy661ixudk5izi"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 3,
},
},
{
Name: "valid query params",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records?filter=text~'test'&sort=-bool",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalItems":2`,
`"items":[{`,
`"id":"al1h9ijdeojtsjy"`,
`"id":"84nmscqy84lsi1t"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 2,
},
},
{
Name: "invalid filter",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records?filter=invalid~'test'",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "expand relations",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records?expand=rel_one,rel_many.rel,missing&perPage=2&sort=created",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":2`,
`"totalPages":2`,
`"totalItems":3`,
`"items":[{`,
`"collectionName":"demo1"`,
`"id":"84nmscqy84lsi1t"`,
`"id":"al1h9ijdeojtsjy"`,
`"expand":{`,
`"rel_one":""`,
`"rel_one":{"`,
`"rel_many":[{`,
`"rel":{`,
`"rel":""`,
`"json":[1,2,3]`,
`"select_many":["optionB","optionC"]`,
`"select_many":["optionB"]`,
// subrel items
`"id":"0yxhwia2amd8gec"`,
`"id":"llvuca81nly1qls"`,
// email visibility should be ignored for superusers even in expanded rels
`"email":"[email protected]"`,
`"email":"[email protected]"`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 8,
},
},
{
Name: "authenticated record model that DOESN'T match the collection list rule",
Method: http.MethodGet,
URL: "/v1/collections/demo3/records",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalItems":0`,
`"items":[]`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
},
},
{
Name: "authenticated record that matches the collection list rule",
Method: http.MethodGet,
URL: "/v1/collections/demo3/records",
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":4`,
`"items":[{`,
`"id":"1tmknxy2868d869"`,
`"id":"lcl9d87w22ml6jy"`,
`"id":"7nwo8tuiatetxdm"`,
`"id":"mk5fmymtx4wsprk"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 4,
},
},
{
Name: "authenticated regular record that matches the collection list rule with hidden field",
Method: http.MethodGet,
URL: "/v1/collections/demo3/records",
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
col.ListRule = types.Pointer("title ~ 'test'")
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":4`,
`"items":[{`,
`"id":"1tmknxy2868d869"`,
`"id":"lcl9d87w22ml6jy"`,
`"id":"7nwo8tuiatetxdm"`,
`"id":"mk5fmymtx4wsprk"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 4,
},
},
{
Name: "authenticated regular record filtering with a hidden field",
Method: http.MethodGet,
URL: "/v1/collections/demo3/records?filter=title~'test'",
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "superuser filtering with a hidden field",
Method: http.MethodGet,
URL: "/v1/collections/demo3/records?filter=title~'test'",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":4`,
`"items":[{`,
`"id":"1tmknxy2868d869"`,
`"id":"lcl9d87w22ml6jy"`,
`"id":"7nwo8tuiatetxdm"`,
`"id":"mk5fmymtx4wsprk"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 4,
},
},
{
Name: ":rule modifer",
Method: http.MethodGet,
URL: "/v1/collections/demo5/records",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":1`,
`"items":[{`,
`"id":"qjeql998mtp1azp"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "multi-match - at least one of (guest - non-satisfied relation filter API rule)",
Method: http.MethodGet,
URL: "/v1/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length?=2"),
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":0`,
`"totalItems":0`,
`"items":[]`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 0,
},
},
{
Name: "multi-match - at least one of (clients)",
Method: http.MethodGet,
URL: "/v1/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length?=2"),
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":1`,
`"items":[{`,
`"id":"qzaqccwrmva4o1n"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "multi-match - all (clients)",
Method: http.MethodGet,
URL: "/v1/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length=2"),
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":0`,
`"totalItems":0`,
`"items":[]`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
},
},
{
Name: "OnRecordsListRequest tx body write check",
Method: http.MethodGet,
URL: "/v1/collections/demo4/records",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.OnRecordsListRequest().BindFunc(func(e *core.RecordsListRequestEvent) error {
original := e.App
return e.App.RunInTransaction(func(txApp core.App) error {
e.App = txApp
defer func() { e.App = original }()
if err := e.Next(); err != nil {
return err
}
return e.BadRequestError("TX_ERROR", nil)
})
})
},
ExpectedStatus: 400,
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
ExpectedContent: []string{"TX_ERROR"},
},
// auth collection
// -----------------------------------------------------------
{
Name: "check email visibility as guest",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":3`,
`"items":[{`,
`"id":"phhq3wr65cap535"`,
`"id":"dc49k6jgejn40h3"`,
`"id":"oos036e9xvqeexy"`,
`"email":"[email protected]"`,
`"emailVisibility":true`,
`"emailVisibility":false`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
`"email":"[email protected]"`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 3,
},
},
{
Name: "check email visibility as any authenticated record",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records",
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":3`,
`"items":[{`,
`"id":"phhq3wr65cap535"`,
`"id":"dc49k6jgejn40h3"`,
`"id":"oos036e9xvqeexy"`,
`"email":"[email protected]"`,
`"emailVisibility":true`,
`"emailVisibility":false`,
},
NotExpectedContent: []string{
`"tokenKey":"`,
`"password":""`,
`"email":"[email protected]"`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 3,
},
},
{
Name: "check email visibility as manage auth record",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":3`,
`"items":[{`,
`"id":"phhq3wr65cap535"`,
`"id":"dc49k6jgejn40h3"`,
`"id":"oos036e9xvqeexy"`,
`"email":"[email protected]"`,
`"email":"[email protected]"`,
`"email":"[email protected]"`,
`"emailVisibility":true`,
`"emailVisibility":false`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 3,
},
},
{
Name: "check email visibility as superuser",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":3`,
`"items":[{`,
`"id":"phhq3wr65cap535"`,
`"id":"dc49k6jgejn40h3"`,
`"id":"oos036e9xvqeexy"`,
`"email":"[email protected]"`,
`"email":"[email protected]"`,
`"email":"[email protected]"`,
`"emailVisibility":true`,
`"emailVisibility":false`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 3,
},
},
{
Name: "check self email visibility resolver",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records",
Headers: map[string]string{
// nologin, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJrcHY3MDlzazJscWJxazgiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZGM0OWs2amdlam40MGgzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.0IefiphujJikMIaWzX0X5VLSWnCN1W6P5g3glRRIVtA",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":3`,
`"items":[{`,
`"id":"phhq3wr65cap535"`,
`"id":"dc49k6jgejn40h3"`,
`"id":"oos036e9xvqeexy"`,
`"email":"[email protected]"`,
`"email":"[email protected]"`,
`"emailVisibility":true`,
`"emailVisibility":false`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 3,
},
},
// view collection
// -----------------------------------------------------------
{
Name: "public view records",
Method: http.MethodGet,
URL: "/v1/collections/view2/records?filter=state=false",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":2`,
`"items":[{`,
`"id":"al1h9ijdeojtsjy"`,
`"id":"imy661ixudk5izi"`,
},
NotExpectedContent: []string{
`"created"`,
`"updated"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 2,
},
},
{
Name: "guest that doesn't match the view collection list rule",
Method: http.MethodGet,
URL: "/v1/collections/view1/records",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":0`,
`"totalItems":0`,
`"items":[]`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
},
},
{
Name: "authenticated record that matches the view collection list rule",
Method: http.MethodGet,
URL: "/v1/collections/view1/records",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":1`,
`"items":[{`,
`"id":"84nmscqy84lsi1t"`,
`"bool":true`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "view collection with numeric ids",
Method: http.MethodGet,
URL: "/v1/collections/numeric_id_view/records",
ExpectedStatus: 200,
ExpectedContent: []string{
`"page":1`,
`"perPage":30`,
`"totalPages":1`,
`"totalItems":2`,
`"items":[{`,
`"id":"1"`,
`"id":"2"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordsListRequest": 1,
"OnRecordEnrich": 2,
},
},
// rate limit checks
// -----------------------------------------------------------
{
Name: "RateLimit rule - view2:list",
Method: http.MethodGet,
URL: "/v1/collections/view2/records",
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 100, Label: "*:list"},
{MaxRequests: 0, Label: "view2:list"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "RateLimit rule - *:list",
Method: http.MethodGet,
URL: "/v1/collections/view2/records",
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 0, Label: "*:list"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestRecordCrudView(t *testing.T) {
t.Parallel()
scenarios := []tests.ApiScenario{
{
Name: "missing collection",
Method: http.MethodGet,
URL: "/v1/collections/missing/records/0yxhwia2amd8gec",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "missing record",
Method: http.MethodGet,
URL: "/v1/collections/demo2/records/missing",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "unauthenticated trying to access nil rule collection (aka. need superuser auth)",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "authenticated record trying to access nil rule collection (aka. need superuser auth)",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "authenticated record that doesn't match the collection view rule",
Method: http.MethodGet,
URL: "/v1/collections/users/records/bgs820n361vj1qd",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "public collection view",
Method: http.MethodGet,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"0yxhwia2amd8gec"`,
`"collectionName":"demo2"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "public collection view (using the collection id)",
Method: http.MethodGet,
URL: "/v1/collections/sz5l5z67tg7gku0/records/0yxhwia2amd8gec",
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"0yxhwia2amd8gec"`,
`"collectionName":"demo2"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "authorized as superuser trying to access nil rule collection view (aka. need superuser auth)",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"imy661ixudk5izi"`,
`"collectionName":"demo1"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "authenticated record that does match the collection view rule",
Method: http.MethodGet,
URL: "/v1/collections/users/records/4q1xlclmfloku33",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"4q1xlclmfloku33"`,
`"collectionName":"users"`,
// owners can always view their email
`"emailVisibility":false`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "expand relations",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records/al1h9ijdeojtsjy?expand=rel_one,rel_many.rel,missing&perPage=2&sort=created",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"al1h9ijdeojtsjy"`,
`"collectionName":"demo1"`,
`"rel_many":[{`,
`"rel_one":{`,
`"collectionName":"users"`,
`"id":"bgs820n361vj1qd"`,
`"expand":{"rel":{`,
`"id":"0yxhwia2amd8gec"`,
`"collectionName":"demo2"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 7,
},
},
{
Name: "OnRecordViewRequest tx body write check",
Method: http.MethodGet,
URL: "/v1/collections/demo1/records/al1h9ijdeojtsjy",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.OnRecordViewRequest().BindFunc(func(e *core.RecordRequestEvent) error {
original := e.App
return e.App.RunInTransaction(func(txApp core.App) error {
e.App = txApp
defer func() { e.App = original }()
if err := e.Next(); err != nil {
return err
}
return e.BadRequestError("TX_ERROR", nil)
})
})
},
ExpectedStatus: 400,
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
ExpectedContent: []string{"TX_ERROR"},
},
// auth collection
// -----------------------------------------------------------
{
Name: "check email visibility as guest",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records/oos036e9xvqeexy",
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"oos036e9xvqeexy"`,
`"emailVisibility":false`,
`"verified":true`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "check email visibility as any authenticated record",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records/oos036e9xvqeexy",
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"oos036e9xvqeexy"`,
`"emailVisibility":false`,
`"verified":true`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
`"email":"[email protected]"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "check email visibility as manage auth record",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records/oos036e9xvqeexy",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"oos036e9xvqeexy"`,
`"emailVisibility":false`,
`"email":"[email protected]"`,
`"verified":true`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "check email visibility as superuser",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records/oos036e9xvqeexy",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"oos036e9xvqeexy"`,
`"emailVisibility":false`,
`"email":"[email protected]"`,
`"verified":true`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "check self email visibility resolver",
Method: http.MethodGet,
URL: "/v1/collections/nologin/records/dc49k6jgejn40h3",
Headers: map[string]string{
// nologin, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJrcHY3MDlzazJscWJxazgiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZGM0OWs2amdlam40MGgzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.0IefiphujJikMIaWzX0X5VLSWnCN1W6P5g3glRRIVtA",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"dc49k6jgejn40h3"`,
`"email":"[email protected]"`,
`"emailVisibility":false`,
`"verified":false`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
// view collection
// -----------------------------------------------------------
{
Name: "public view record",
Method: http.MethodGet,
URL: "/v1/collections/view2/records/84nmscqy84lsi1t",
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"84nmscqy84lsi1t"`,
`"state":true`,
`"file_many":["`,
`"rel_many":["`,
},
NotExpectedContent: []string{
`"created"`,
`"updated"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "guest that doesn't match the view collection view rule",
Method: http.MethodGet,
URL: "/v1/collections/view1/records/84nmscqy84lsi1t",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "authenticated record that matches the view collection view rule",
Method: http.MethodGet,
URL: "/v1/collections/view1/records/84nmscqy84lsi1t",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"84nmscqy84lsi1t"`,
`"bool":true`,
`"text":"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "view record with numeric id",
Method: http.MethodGet,
URL: "/v1/collections/numeric_id_view/records/1",
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"1"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordViewRequest": 1,
"OnRecordEnrich": 1,
},
},
// rate limit checks
// -----------------------------------------------------------
{
Name: "RateLimit rule - numeric_id_view:view",
Method: http.MethodGet,
URL: "/v1/collections/numeric_id_view/records/1",
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 100, Label: "*:view"},
{MaxRequests: 0, Label: "numeric_id_view:view"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "RateLimit rule - *:view",
Method: http.MethodGet,
URL: "/v1/collections/numeric_id_view/records/1",
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 0, Label: "*:view"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestRecordCrudDelete(t *testing.T) {
t.Parallel()
ensureDeletedFiles := func(app *tests.TestApp, collectionId string, recordId string) {
storageDir := filepath.Join(app.DataDir(), "storage", collectionId, recordId)
entries, _ := os.ReadDir(storageDir)
if len(entries) != 0 {
t.Errorf("Expected empty/deleted dir, found: %d\n%v", len(entries), entries)
}
}
scenarios := []tests.ApiScenario{
{
Name: "missing collection",
Method: http.MethodDelete,
URL: "/v1/collections/missing/records/0yxhwia2amd8gec",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "missing record",
Method: http.MethodDelete,
URL: "/v1/collections/demo2/records/missing",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "unauthenticated trying to delete nil rule collection (aka. need superuser auth)",
Method: http.MethodDelete,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "authenticated record trying to delete nil rule collection (aka. need superuser auth)",
Method: http.MethodDelete,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "authenticated record that doesn't match the collection delete rule",
Method: http.MethodDelete,
URL: "/v1/collections/users/records/bgs820n361vj1qd",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "trying to delete a view collection record",
Method: http.MethodDelete,
URL: "/v1/collections/view1/records/imy661ixudk5izi",
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "public collection record delete",
Method: http.MethodDelete,
URL: "/v1/collections/nologin/records/dc49k6jgejn40h3",
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordDeleteRequest": 1,
"OnModelDelete": 1,
"OnModelDeleteExecute": 1,
"OnModelAfterDeleteSuccess": 1,
"OnRecordDelete": 1,
"OnRecordDeleteExecute": 1,
"OnRecordAfterDeleteSuccess": 1,
},
},
{
Name: "public collection record delete (using the collection id as identifier)",
Method: http.MethodDelete,
URL: "/v1/collections/kpv709sk2lqbqk8/records/dc49k6jgejn40h3",
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordDeleteRequest": 1,
"OnModelDelete": 1,
"OnModelDeleteExecute": 1,
"OnModelAfterDeleteSuccess": 1,
"OnRecordDelete": 1,
"OnRecordDeleteExecute": 1,
"OnRecordAfterDeleteSuccess": 1,
},
},
{
Name: "authorized as superuser trying to delete nil rule collection view (aka. need superuser auth)",
Method: http.MethodDelete,
URL: "/v1/collections/clients/records/o1y0dd0spd786md",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordDeleteRequest": 1,
"OnModelDelete": 1,
"OnModelDeleteExecute": 1,
"OnModelAfterDeleteSuccess": 1,
"OnRecordDelete": 1,
"OnRecordDeleteExecute": 1,
"OnRecordAfterDeleteSuccess": 1,
},
},
{
Name: "OnRecordDeleteRequest tx body write check",
Method: http.MethodDelete,
URL: "/v1/collections/clients/records/o1y0dd0spd786md",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.OnRecordDeleteRequest().BindFunc(func(e *core.RecordRequestEvent) error {
original := e.App
return e.App.RunInTransaction(func(txApp core.App) error {
e.App = txApp
defer func() { e.App = original }()
if err := e.Next(); err != nil {
return err
}
return e.BadRequestError("TX_ERROR", nil)
})
})
},
ExpectedStatus: 400,
ExpectedEvents: map[string]int{"OnRecordDeleteRequest": 1},
ExpectedContent: []string{"TX_ERROR"},
},
{
Name: "authenticated record that match the collection delete rule",
Method: http.MethodDelete,
URL: "/v1/collections/users/records/4q1xlclmfloku33",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordDeleteRequest": 1,
"OnModelDelete": 1,
"OnModelDeleteExecute": 1,
"OnModelAfterDeleteSuccess": 1,
"OnRecordDelete": 1,
"OnRecordDeleteExecute": 1,
"OnRecordAfterDeleteSuccess": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnRecordUpdateExecute": 1,
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
ensureDeletedFiles(app, "_users_auth_", "4q1xlclmfloku33")
},
},
{
Name: "@request :isset (rule failure check)",
Method: http.MethodDelete,
URL: "/v1/collections/demo5/records/la4y2w4o98acwuj",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "@request :isset (rule pass check)",
Method: http.MethodDelete,
URL: "/v1/collections/demo5/records/la4y2w4o98acwuj?test=1",
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordDeleteRequest": 1,
"OnModelDelete": 1,
"OnModelDeleteExecute": 1,
"OnModelAfterDeleteSuccess": 1,
"OnRecordDelete": 1,
"OnRecordDeleteExecute": 1,
"OnRecordAfterDeleteSuccess": 1,
},
},
// cascade delete checks
// -----------------------------------------------------------
{
Name: "trying to delete a record while being part of a non-cascade required relation",
Method: http.MethodDelete,
URL: "/v1/collections/demo3/records/7nwo8tuiatetxdm",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordDeleteRequest": 1,
"OnModelDelete": 2, // the record itself + rel_one_cascade of test1 record
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteError": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteError": 2,
"OnModelUpdate": 2, // self_rel_many update of test1 record + rel_one_cascade demo4 cascaded in demo5
"OnModelUpdateExecute": 2,
"OnModelAfterUpdateError": 2,
"OnRecordUpdate": 2,
"OnRecordUpdateExecute": 2,
"OnRecordAfterUpdateError": 2,
},
},
{
Name: "delete a record with non-cascade references",
Method: http.MethodDelete,
URL: "/v1/collections/demo3/records/1tmknxy2868d869",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordDeleteRequest": 1,
"OnModelDelete": 1,
"OnModelDeleteExecute": 1,
"OnModelAfterDeleteSuccess": 1,
"OnRecordDelete": 1,
"OnRecordDeleteExecute": 1,
"OnRecordAfterDeleteSuccess": 1,
"OnModelUpdate": 2,
"OnModelUpdateExecute": 2,
"OnModelAfterUpdateSuccess": 2,
"OnRecordUpdate": 2,
"OnRecordUpdateExecute": 2,
"OnRecordAfterUpdateSuccess": 2,
},
},
{
Name: "delete a record with cascade references",
Method: http.MethodDelete,
URL: "/v1/collections/users/records/oap640cot4yru2s",
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
Delay: 100 * time.Millisecond,
ExpectedStatus: 204,
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordDeleteRequest": 1,
"OnModelDelete": 2,
"OnModelDeleteExecute": 2,
"OnModelAfterDeleteSuccess": 2,
"OnRecordDelete": 2,
"OnRecordDeleteExecute": 2,
"OnRecordAfterDeleteSuccess": 2,
"OnModelUpdate": 2,
"OnModelUpdateExecute": 2,
"OnModelAfterUpdateSuccess": 2,
"OnRecordUpdate": 2,
"OnRecordUpdateExecute": 2,
"OnRecordAfterUpdateSuccess": 2,
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
recId := "84nmscqy84lsi1t"
rec, _ := app.FindRecordById("demo1", recId, nil)
if rec != nil {
t.Errorf("Expected record %s to be cascade deleted", recId)
}
ensureDeletedFiles(app, "wsmn24bux7wo113", recId)
ensureDeletedFiles(app, "_users_auth_", "oap640cot4yru2s")
},
},
// rate limit checks
// -----------------------------------------------------------
{
Name: "RateLimit rule - demo5:delete",
Method: http.MethodDelete,
URL: "/v1/collections/demo5/records/la4y2w4o98acwuj?test=1",
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 100, Label: "*:delete"},
{MaxRequests: 0, Label: "demo5:delete"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "RateLimit rule - *:delete",
Method: http.MethodDelete,
URL: "/v1/collections/demo5/records/la4y2w4o98acwuj?test=1",
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 0, Label: "*:delete"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestRecordCrudCreate(t *testing.T) {
t.Parallel()
formData, mp, err := tests.MockMultipartData(map[string]string{
"title": "title_test",
}, "files")
if err != nil {
t.Fatal(err)
}
formData2, mp2, err2 := tests.MockMultipartData(map[string]string{
router.JSONPayloadKey: `{"title": "title_test2", "testPayload": 123}`,
}, "files")
if err2 != nil {
t.Fatal(err2)
}
formData3, mp3, err3 := tests.MockMultipartData(map[string]string{
router.JSONPayloadKey: `{"title": "title_test3", "testPayload": 123}`,
}, "files")
if err3 != nil {
t.Fatal(err3)
}
scenarios := []tests.ApiScenario{
{
Name: "missing collection",
Method: http.MethodPost,
URL: "/v1/collections/missing/records",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "guest trying to access nil-rule collection",
Method: http.MethodPost,
URL: "/v1/collections/demo1/records",
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "auth record trying to access nil-rule collection",
Method: http.MethodPost,
URL: "/v1/collections/demo1/records",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "trying to create a new view collection record",
Method: http.MethodPost,
URL: "/v1/collections/view1/records",
Body: strings.NewReader(`{"text":"new"}`),
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "submit invalid body",
Method: http.MethodPost,
URL: "/v1/collections/demo2/records",
Body: strings.NewReader(`{"`),
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "submit nil body",
Method: http.MethodPost,
URL: "/v1/collections/demo2/records",
Body: nil,
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"title":{"code":"validation_required"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelValidate": 1,
"OnModelAfterCreateError": 1,
"OnRecordCreate": 1,
"OnRecordValidate": 1,
"OnRecordAfterCreateError": 1,
},
},
{
Name: "submit empty json body",
Method: http.MethodPost,
URL: "/v1/collections/nologin/records",
Body: strings.NewReader(`{}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"email":{"code":"validation_required"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnRecordCreate": 1,
"OnModelCreate": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnModelAfterCreateError": 1,
"OnRecordAfterCreateError": 1,
},
},
{
Name: "guest submit in public collection",
Method: http.MethodPost,
URL: "/v1/collections/demo2/records",
Body: strings.NewReader(`{"title":"new"}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":`,
`"title":"new"`,
`"active":false`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "guest trying to submit in restricted collection",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: strings.NewReader(`{"title":"test123"}`),
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "auth record submit in restricted collection (rule failure check)",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: strings.NewReader(`{"title":"test123"}`),
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "auth record submit in restricted collection (rule pass check) + expand relations",
Method: http.MethodPost,
URL: "/v1/collections/demo4/records?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required",
Body: strings.NewReader(`{
"title":"test123",
"rel_one_no_cascade":"mk5fmymtx4wsprk",
"rel_one_no_cascade_required":"7nwo8tuiatetxdm",
"rel_one_cascade":"mk5fmymtx4wsprk",
"rel_many_no_cascade":"mk5fmymtx4wsprk",
"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"],
"rel_many_cascade":"lcl9d87w22ml6jy"
}`),
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":`,
`"title":"test123"`,
`"expand":{}`, // empty expand even because of the query param
`"rel_one_no_cascade":"mk5fmymtx4wsprk"`,
`"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`,
`"rel_one_cascade":"mk5fmymtx4wsprk"`,
`"rel_many_no_cascade":["mk5fmymtx4wsprk"]`,
`"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"]`,
`"rel_many_cascade":["lcl9d87w22ml6jy"]`,
},
NotExpectedContent: []string{
// the users auth records don't have access to view the demo3 expands
`"missing"`,
`"id":"mk5fmymtx4wsprk"`,
`"id":"7nwo8tuiatetxdm"`,
`"id":"lcl9d87w22ml6jy"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "superuser submit in restricted collection (rule skip check) + expand relations",
Method: http.MethodPost,
URL: "/v1/collections/demo4/records?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required",
Body: strings.NewReader(`{
"title":"test123",
"rel_one_no_cascade":"mk5fmymtx4wsprk",
"rel_one_no_cascade_required":"7nwo8tuiatetxdm",
"rel_one_cascade":"mk5fmymtx4wsprk",
"rel_many_no_cascade":"mk5fmymtx4wsprk",
"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"],
"rel_many_cascade":"lcl9d87w22ml6jy"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":`,
`"title":"test123"`,
`"rel_one_no_cascade":"mk5fmymtx4wsprk"`,
`"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`,
`"rel_one_cascade":"mk5fmymtx4wsprk"`,
`"rel_many_no_cascade":["mk5fmymtx4wsprk"]`,
`"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"]`,
`"rel_many_cascade":["lcl9d87w22ml6jy"]`,
`"expand":{`,
`"id":"mk5fmymtx4wsprk"`,
`"id":"7nwo8tuiatetxdm"`,
`"id":"lcl9d87w22ml6jy"`,
},
NotExpectedContent: []string{
`"missing"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 4,
},
},
{
Name: "superuser submit via multipart form data",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: formData,
Headers: map[string]string{
"Content-Type": mp.FormDataContentType(),
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"`,
`"title":"title_test"`,
`"files":["`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "submit via multipart form data with @jsonPayload key and unsatisfied @request.body rule",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: formData2,
Headers: map[string]string{
"Content-Type": mp2.FormDataContentType(),
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
collection, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatalf("failed to find demo3 collection: %v", err)
}
collection.CreateRule = types.Pointer("@request.body.testPayload != 123")
if err := app.Save(collection); err != nil {
t.Fatalf("failed to update demo3 collection create rule: %v", err)
}
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "submit via multipart form data with @jsonPayload key and satisfied @request.body rule",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: formData3,
Headers: map[string]string{
"Content-Type": mp3.FormDataContentType(),
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
collection, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatalf("failed to find demo3 collection: %v", err)
}
collection.CreateRule = types.Pointer("@request.body.testPayload = 123")
if err := app.Save(collection); err != nil {
t.Fatalf("failed to update demo3 collection create rule: %v", err)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"`,
`"title":"title_test3"`,
`"files":["`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "unique field error check",
Method: http.MethodPost,
URL: "/v1/collections/demo2/records",
Body: strings.NewReader(`{
"title":"test2"
}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"title":{`,
`"code":"validation_not_unique"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateError": 1,
"OnModelValidate": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateError": 1,
"OnRecordValidate": 1,
},
},
{
Name: "OnRecordCreateRequest tx body write check",
Method: http.MethodPost,
URL: "/v1/collections/demo2/records",
Body: strings.NewReader(`{"title":"new"}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.OnRecordCreateRequest().BindFunc(func(e *core.RecordRequestEvent) error {
original := e.App
return e.App.RunInTransaction(func(txApp core.App) error {
e.App = txApp
defer func() { e.App = original }()
if err := e.Next(); err != nil {
return err
}
return e.BadRequestError("TX_ERROR", nil)
})
})
},
ExpectedStatus: 400,
ExpectedEvents: map[string]int{"OnRecordCreateRequest": 1},
ExpectedContent: []string{"TX_ERROR"},
},
// ID checks
// -----------------------------------------------------------
{
Name: "invalid custom insertion id (less than 15 chars)",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: strings.NewReader(`{
"id": "12345678901234",
"title": "test"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"id":{"code":"validation_min_text_constraint"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelValidate": 1,
"OnModelAfterCreateError": 1,
"OnRecordCreate": 1,
"OnRecordValidate": 1,
"OnRecordAfterCreateError": 1,
},
},
{
Name: "invalid custom insertion id (more than 15 chars)",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: strings.NewReader(`{
"id": "1234567890123456",
"title": "test"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"id":{"code":"validation_max_text_constraint"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelValidate": 1,
"OnModelAfterCreateError": 1,
"OnRecordCreate": 1,
"OnRecordValidate": 1,
"OnRecordAfterCreateError": 1,
},
},
{
Name: "valid custom insertion id (exactly 15 chars)",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: strings.NewReader(`{
"id": "123456789012345",
"title": "test"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"123456789012345"`,
`"title":"test"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "valid custom insertion id existing in another non-auth collection",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: strings.NewReader(`{
"id": "0yxhwia2amd8gec",
"title": "test"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"0yxhwia2amd8gec"`,
`"title":"test"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "valid custom insertion auth id duplicating in another auth collection",
Method: http.MethodPost,
URL: "/v1/collections/users/records",
Body: strings.NewReader(`{
"id":"o1y0dd0spd786md",
"title":"test",
"password":"1234567890",
"passwordConfirm":"1234567890"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"id":{"code":"validation_invalid_auth_id"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1, // unique constraints are handled on db level
"OnModelAfterCreateError": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateError": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
},
},
// check whether if @request.body modifer fields are properly resolved
// -----------------------------------------------------------
{
Name: "@request.body.field with compute modifers (rule failure check)",
Method: http.MethodPost,
URL: "/v1/collections/demo5/records",
Body: strings.NewReader(`{
"total+":4,
"total-":2
}`),
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "@request.body.field with compute modifers (rule pass check)",
Method: http.MethodPost,
URL: "/v1/collections/demo5/records",
Body: strings.NewReader(`{
"total+":4,
"total-":1
}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"`,
`"collectionName":"demo5"`,
`"total":3`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
// auth records
// -----------------------------------------------------------
{
Name: "auth record with invalid email",
Method: http.MethodPost,
URL: "/v1/collections/users/records",
Body: strings.NewReader(`{
"email":"invalid",
"username":"Users75657"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"email":{"code":"validation_is_email"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelValidate": 1,
"OnModelAfterCreateError": 1,
"OnRecordCreate": 1,
"OnRecordValidate": 1,
"OnRecordAfterCreateError": 1,
},
},
{
Name: "auth record with invalid record fields",
Method: http.MethodPost,
URL: "/v1/collections/users/records",
Body: strings.NewReader(`{
"email":"[email protected]",
"rel":"invalid"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"rel":{"code":`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelValidate": 1,
"OnModelAfterCreateError": 1,
"OnRecordCreate": 1,
"OnRecordValidate": 1,
"OnRecordAfterCreateError": 1,
},
},
{
Name: "auth record with explicit verified state by guest",
Method: http.MethodPost,
URL: "/v1/collections/users/records",
Body: strings.NewReader(`{
"email":"[email protected]",
"verified":true
}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"verified":{"code":`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
// no validation hooks because it should fail before save by the form auth fields validator
},
},
{
Name: "auth record with valid data and explicitly verified state by random user",
Method: http.MethodPost,
URL: "/v1/collections/users/records",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
Body: strings.NewReader(`{
"password":"12345678",
"passwordConfirm":"12345678",
"emailVisibility":true,
"verified":true
}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"verified":{"code":`,
},
NotExpectedContent: []string{
`"emailVisibility":{"code":`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
// no validation hooks because it should fail before save by the form auth fields validator
},
},
{
Name: "auth record with valid data by superuser",
Method: http.MethodPost,
URL: "/v1/collections/users/records",
Body: strings.NewReader(`{
"id":"o1o1y0pd78686mq",
"username":"test.valid",
"email":"[email protected]",
"password":"12345678",
"passwordConfirm":"12345678",
"rel":"achvryl401bhse3",
"emailVisibility":true,
"verified":true
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"o1o1y0pd78686mq"`,
`"username":"test.valid"`,
`"email":"[email protected]"`,
`"rel":"achvryl401bhse3"`,
`"emailVisibility":true`,
`"verified":true`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
`"passwordConfirm"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "auth record with valid data by auth record with manage access",
Method: http.MethodPost,
URL: "/v1/collections/nologin/records",
Body: strings.NewReader(`{
"email":"[email protected]",
"password":"12345678",
"passwordConfirm":"12345678",
"name":"test_name",
"emailVisibility":true,
"verified":true
}`),
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"`,
`"username":"`,
`"email":"[email protected]"`,
`"name":"test_name"`,
`"emailVisibility":true`,
`"verified":true`,
},
NotExpectedContent: []string{
`"tokenKey"`,
`"password"`,
`"passwordConfirm"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
// ensure that hidden fields cannot be set by non-superusers
// -----------------------------------------------------------
{
Name: "create with hidden field as regular user",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: strings.NewReader(`{
"id": "abcde1234567890",
"title": "test_create"
}`),
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
record, err := app.FindRecordById("demo3", "abcde1234567890")
if err != nil {
t.Fatal(err)
}
// ensure that the title wasn't saved
if v := record.GetString("title"); v != "" {
t.Fatalf("Expected empty title, got %q", v)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"abcde1234567890"`,
},
NotExpectedContent: []string{
`"title"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "create with hidden field as superuser",
Method: http.MethodPost,
URL: "/v1/collections/demo3/records",
Body: strings.NewReader(`{
"id": "abcde1234567890",
"title": "test_create"
}`),
Headers: map[string]string{
// superusers, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
record, err := app.FindRecordById("demo3", "abcde1234567890")
if err != nil {
t.Fatal(err)
}
// ensure that the title was saved
if v := record.GetString("title"); v != "test_create" {
t.Fatalf("Expected title %q, got %q", "test_create", v)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"abcde1234567890"`,
`"title":"test_create"`,
},
NotExpectedContent: []string{},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordCreateRequest": 1,
"OnModelCreate": 1,
"OnModelCreateExecute": 1,
"OnModelAfterCreateSuccess": 1,
"OnRecordCreate": 1,
"OnRecordCreateExecute": 1,
"OnRecordAfterCreateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
// rate limit checks
// -----------------------------------------------------------
{
Name: "RateLimit rule - demo2:create",
Method: http.MethodPost,
URL: "/v1/collections/demo2/records",
Body: strings.NewReader(`{"title":"new"}`),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 100, Label: "*:create"},
{MaxRequests: 0, Label: "demo2:create"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "RateLimit rule - *:create",
Method: http.MethodPost,
URL: "/v1/collections/demo2/records",
Body: strings.NewReader(`{"title":"new"}`),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 0, Label: "*:create"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
// dynamic body limit checks
// -----------------------------------------------------------
{
Name: "body > collection BodyLimit",
Method: http.MethodPost,
URL: "/v1/collections/demo1/records",
// the exact body doesn't matter as long as it returns 413
Body: bytes.NewReader(make([]byte, apis.DefaultMaxBodySize+5+20+2+1)),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
collection, err := app.FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
// adjust field sizes for the test
// ---
fileOneField := collection.Fields.GetByName("file_one").(*core.FileField)
fileOneField.MaxSize = 5
fileManyField := collection.Fields.GetByName("file_many").(*core.FileField)
fileManyField.MaxSize = 10
fileManyField.MaxSelect = 2
jsonField := collection.Fields.GetByName("json").(*core.JSONField)
jsonField.MaxSize = 2
err = app.Save(collection)
if err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 413,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "body <= collection BodyLimit",
Method: http.MethodPost,
URL: "/v1/collections/demo1/records",
// the exact body doesn't matter as long as it doesn't return 413
Body: bytes.NewReader(make([]byte, apis.DefaultMaxBodySize+5+20+2)),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
collection, err := app.FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
// adjust field sizes for the test
// ---
fileOneField := collection.Fields.GetByName("file_one").(*core.FileField)
fileOneField.MaxSize = 5
fileManyField := collection.Fields.GetByName("file_many").(*core.FileField)
fileManyField.MaxSize = 10
fileManyField.MaxSelect = 2
jsonField := collection.Fields.GetByName("json").(*core.JSONField)
jsonField.MaxSize = 2
err = app.Save(collection)
if err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}
func TestRecordCrudUpdate(t *testing.T) {
t.Parallel()
formData, mp, err := tests.MockMultipartData(map[string]string{
"title": "title_test",
}, "files")
if err != nil {
t.Fatal(err)
}
formData2, mp2, err2 := tests.MockMultipartData(map[string]string{
router.JSONPayloadKey: `{"title": "title_test2", "testPayload": 123}`,
}, "files")
if err2 != nil {
t.Fatal(err2)
}
formData3, mp3, err3 := tests.MockMultipartData(map[string]string{
router.JSONPayloadKey: `{"title": "title_test3", "testPayload": 123, "files":"300_JdfBOieXAW.png"}`,
}, "files")
if err3 != nil {
t.Fatal(err3)
}
scenarios := []tests.ApiScenario{
{
Name: "missing collection",
Method: http.MethodPatch,
URL: "/v1/collections/missing/records/0yxhwia2amd8gec",
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "guest trying to access nil-rule collection record",
Method: http.MethodPatch,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "auth record trying to access nil-rule collection",
Method: http.MethodPatch,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 403,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "trying to update a view collection record",
Method: http.MethodPatch,
URL: "/v1/collections/view1/records/imy661ixudk5izi",
Body: strings.NewReader(`{"text":"new"}`),
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "submit invalid body",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
Body: strings.NewReader(`{"`),
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "submit nil body (aka. no fields change)",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
Body: nil,
ExpectedStatus: 200,
ExpectedContent: []string{
`"collectionName":"demo2"`,
`"id":"0yxhwia2amd8gec"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "submit empty body (aka. no fields change)",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
Body: strings.NewReader(`{}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"collectionName":"demo2"`,
`"id":"0yxhwia2amd8gec"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "trigger field validation",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
Body: strings.NewReader(`{"title":"a"}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`data":{`,
`"title":{"code":"validation_min_text_constraint"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelValidate": 1,
"OnModelAfterUpdateError": 1,
"OnRecordUpdate": 1,
"OnRecordValidate": 1,
"OnRecordAfterUpdateError": 1,
},
},
{
Name: "guest submit in public collection",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
Body: strings.NewReader(`{"title":"new"}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"0yxhwia2amd8gec"`,
`"title":"new"`,
`"active":true`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "guest trying to submit in restricted collection",
Method: http.MethodPatch,
URL: "/v1/collections/demo3/records/mk5fmymtx4wsprk",
Body: strings.NewReader(`{"title":"new"}`),
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "auth record submit in restricted collection (rule failure check)",
Method: http.MethodPatch,
URL: "/v1/collections/demo3/records/mk5fmymtx4wsprk",
Body: strings.NewReader(`{"title":"new"}`),
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "auth record submit in restricted collection (rule pass check) + expand relations",
Method: http.MethodPatch,
URL: "/v1/collections/demo4/records/i9naidtvr6qsgb4?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required",
Body: strings.NewReader(`{
"title":"test123",
"rel_one_no_cascade":"mk5fmymtx4wsprk",
"rel_one_no_cascade_required":"7nwo8tuiatetxdm",
"rel_one_cascade":"mk5fmymtx4wsprk",
"rel_many_no_cascade":"mk5fmymtx4wsprk",
"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"],
"rel_many_cascade":"lcl9d87w22ml6jy"
}`),
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"i9naidtvr6qsgb4"`,
`"title":"test123"`,
`"expand":{}`, // empty expand even because of the query param
`"rel_one_no_cascade":"mk5fmymtx4wsprk"`,
`"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`,
`"rel_one_cascade":"mk5fmymtx4wsprk"`,
`"rel_many_no_cascade":["mk5fmymtx4wsprk"]`,
`"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"]`,
`"rel_many_cascade":["lcl9d87w22ml6jy"]`,
},
NotExpectedContent: []string{
// the users auth records don't have access to view the demo3 expands
`"missing"`,
`"id":"mk5fmymtx4wsprk"`,
`"id":"7nwo8tuiatetxdm"`,
`"id":"lcl9d87w22ml6jy"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "superuser submit in restricted collection (rule skip check) + expand relations",
Method: http.MethodPatch,
URL: "/v1/collections/demo4/records/i9naidtvr6qsgb4?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required",
Body: strings.NewReader(`{
"title":"test123",
"rel_one_no_cascade":"mk5fmymtx4wsprk",
"rel_one_no_cascade_required":"7nwo8tuiatetxdm",
"rel_one_cascade":"mk5fmymtx4wsprk",
"rel_many_no_cascade":"mk5fmymtx4wsprk",
"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"],
"rel_many_cascade":"lcl9d87w22ml6jy"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"i9naidtvr6qsgb4"`,
`"title":"test123"`,
`"rel_one_no_cascade":"mk5fmymtx4wsprk"`,
`"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`,
`"rel_one_cascade":"mk5fmymtx4wsprk"`,
`"rel_many_no_cascade":["mk5fmymtx4wsprk"]`,
`"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"]`,
`"rel_many_cascade":["lcl9d87w22ml6jy"]`,
`"expand":{`,
`"id":"mk5fmymtx4wsprk"`,
`"id":"7nwo8tuiatetxdm"`,
`"id":"lcl9d87w22ml6jy"`,
},
NotExpectedContent: []string{
`"missing"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 4,
},
},
{
Name: "superuser submit via multipart form data",
Method: http.MethodPatch,
URL: "/v1/collections/demo3/records/mk5fmymtx4wsprk",
Body: formData,
Headers: map[string]string{
"Content-Type": mp.FormDataContentType(),
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"mk5fmymtx4wsprk"`,
`"title":"title_test"`,
`"files":["`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "submit via multipart form data with @jsonPayload key and unsatisfied @request.body rule",
Method: http.MethodPatch,
URL: "/v1/collections/demo3/records/mk5fmymtx4wsprk",
Body: formData2,
Headers: map[string]string{
"Content-Type": mp2.FormDataContentType(),
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
collection, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatalf("failed to find demo3 collection: %v", err)
}
collection.UpdateRule = types.Pointer("@request.body.testPayload != 123")
if err := app.Save(collection); err != nil {
t.Fatalf("failed to update demo3 collection update rule: %v", err)
}
},
ExpectedStatus: 404,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "submit via multipart form data with @jsonPayload key and satisfied @request.body rule",
Method: http.MethodPatch,
URL: "/v1/collections/demo3/records/mk5fmymtx4wsprk",
Body: formData3,
Headers: map[string]string{
"Content-Type": mp3.FormDataContentType(),
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
collection, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatalf("failed to find demo3 collection: %v", err)
}
collection.UpdateRule = types.Pointer("@request.body.testPayload = 123")
if err := app.Save(collection); err != nil {
t.Fatalf("failed to update demo3 collection update rule: %v", err)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"mk5fmymtx4wsprk"`,
`"title":"title_test3"`,
`"files":["`,
`"300_JdfBOieXAW.png"`,
`"tmpfile_`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "OnRecordUpdateRequest tx body write check",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
Body: strings.NewReader(`{"title":"new"}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.OnRecordUpdateRequest().BindFunc(func(e *core.RecordRequestEvent) error {
original := e.App
return e.App.RunInTransaction(func(txApp core.App) error {
e.App = txApp
defer func() { e.App = original }()
if err := e.Next(); err != nil {
return err
}
return e.BadRequestError("TX_ERROR", nil)
})
})
},
ExpectedStatus: 400,
ExpectedEvents: map[string]int{"OnRecordUpdateRequest": 1},
ExpectedContent: []string{"TX_ERROR"},
},
{
Name: "try to change the id of an existing record",
Method: http.MethodPatch,
URL: "/v1/collections/demo3/records/mk5fmymtx4wsprk",
Body: strings.NewReader(`{
"id": "mk5fmymtx4wspra"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"id":{"code":"validation_pk_change"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelValidate": 1,
"OnModelAfterUpdateError": 1,
"OnRecordUpdate": 1,
"OnRecordValidate": 1,
"OnRecordAfterUpdateError": 1,
},
},
{
Name: "unique field error check",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/llvuca81nly1qls",
Body: strings.NewReader(`{
"title":"test2"
}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"title":{`,
`"code":"validation_not_unique"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateError": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateError": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
},
},
// check whether if @request.body modifer fields are properly resolved
// -----------------------------------------------------------
{
Name: "@request.body.field with compute modifers (rule failure check)",
Method: http.MethodPatch,
URL: "/v1/collections/demo5/records/la4y2w4o98acwuj",
Body: strings.NewReader(`{
"total+":3,
"total-":1
}`),
ExpectedStatus: 404,
ExpectedContent: []string{
`"data":{}`,
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "@request.body.field with compute modifers (rule pass check)",
Method: http.MethodPatch,
URL: "/v1/collections/demo5/records/la4y2w4o98acwuj",
Body: strings.NewReader(`{
"total+":2,
"total-":1
}`),
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"la4y2w4o98acwuj"`,
`"collectionName":"demo5"`,
`"total":3`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
// auth records
// -----------------------------------------------------------
{
Name: "auth record with invalid email",
Method: http.MethodPatch,
URL: "/v1/collections/users/records/bgs820n361vj1qd",
Body: strings.NewReader(`{
"email":"invalid",
"verified":false
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"email":{"code":"validation_is_email"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelValidate": 1,
"OnModelAfterUpdateError": 1,
"OnRecordUpdate": 1,
"OnRecordValidate": 1,
"OnRecordAfterUpdateError": 1,
},
},
{
Name: "auth record with invalid record fields",
Method: http.MethodPatch,
URL: "/v1/collections/users/records/bgs820n361vj1qd",
Body: strings.NewReader(`{
"rel":"invalid"
}`),
Headers: map[string]string{
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"rel":{"code":`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelValidate": 1,
"OnModelAfterUpdateError": 1,
"OnRecordUpdate": 1,
"OnRecordValidate": 1,
"OnRecordAfterUpdateError": 1,
},
},
{
Name: "try to set verified by guest",
Method: http.MethodPatch,
URL: "/v1/collections/nologin/records/phhq3wr65cap535",
Body: strings.NewReader(`{
"emailVisibility":true,
"verified":true
}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"verified":{"code":`,
},
NotExpectedContent: []string{
`"emailVisibility":{"code":`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
},
},
{
Name: "try to set verified by auth record (owner)",
Method: http.MethodPatch,
URL: "/v1/collections/users/records/4q1xlclmfloku33",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
},
Body: strings.NewReader(`{
"emailVisibility":true,
"verified":true
}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"verified":{"code":`,
},
NotExpectedContent: []string{
`"emailVisibility":{"code":`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
},
},
{
Name: "try to unset/downgrade email and verified fields (owner)",
Method: http.MethodPatch,
URL: "/v1/collections/users/records/oap640cot4yru2s",
Headers: map[string]string{
// users, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoib2FwNjQwY290NHlydTJzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.h9l_1A2r42IIOOi-5IjuzCGuUhlMmxdFDVhncE3FUqg",
},
Body: strings.NewReader(`{
"email":"",
"verified":false
}`),
ExpectedStatus: 400,
ExpectedContent: []string{
`"data":{`,
`"email":{"code":`,
`"verified":{"code":`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
},
},
// ensure that hidden fields cannot be set by non-superusers
// -----------------------------------------------------------
{
Name: "update with hidden field as regular user",
Method: http.MethodPatch,
URL: "/v1/collections/demo3/records/1tmknxy2868d869",
Body: strings.NewReader(`{
"title": "test_update"
}`),
Headers: map[string]string{
// clients, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJ2ODUxcTRyNzkwcmhrbmwiLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiZ2szOTBxZWdzNHk0N3duIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.xCGkWuACPNAEUBLQVK4KKp72HzA2aOtWZnP47iBs5os",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
record, err := app.FindRecordById("demo3", "1tmknxy2868d869")
if err != nil {
t.Fatal(err)
}
// ensure that the title wasn't saved
if v := record.GetString("title"); v != "test1" {
t.Fatalf("Expected no title change, got %q", v)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"1tmknxy2868d869"`,
},
NotExpectedContent: []string{
`"title"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
{
Name: "update with hidden field as superuser",
Method: http.MethodPatch,
URL: "/v1/collections/demo3/records/1tmknxy2868d869",
Body: strings.NewReader(`{
"title": "test_update"
}`),
Headers: map[string]string{
// superusers, [email protected]
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
col, err := app.FindCollectionByNameOrId("demo3")
if err != nil {
t.Fatal(err)
}
// mock hidden field
col.Fields.GetByName("title").SetHidden(true)
if err = app.Save(col); err != nil {
t.Fatal(err)
}
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
record, err := app.FindRecordById("demo3", "1tmknxy2868d869")
if err != nil {
t.Fatal(err)
}
// ensure that the title has been updated
if v := record.GetString("title"); v != "test_update" {
t.Fatalf("Expected title %q, got %q", "test_update", v)
}
},
ExpectedStatus: 200,
ExpectedContent: []string{
`"id":"1tmknxy2868d869"`,
`"title":"test_update"`,
},
ExpectedEvents: map[string]int{
"*": 0,
"OnRecordUpdateRequest": 1,
"OnModelUpdate": 1,
"OnModelUpdateExecute": 1,
"OnModelAfterUpdateSuccess": 1,
"OnRecordUpdate": 1,
"OnRecordUpdateExecute": 1,
"OnRecordAfterUpdateSuccess": 1,
"OnModelValidate": 1,
"OnRecordValidate": 1,
"OnRecordEnrich": 1,
},
},
// rate limit checks
// -----------------------------------------------------------
{
Name: "RateLimit rule - demo2:update",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
Body: strings.NewReader(`{"title":"new"}`),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 100, Label: "*:update"},
{MaxRequests: 0, Label: "demo2:update"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "RateLimit rule - *:update",
Method: http.MethodPatch,
URL: "/v1/collections/demo2/records/0yxhwia2amd8gec",
Body: strings.NewReader(`{"title":"new"}`),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().RateLimits.Enabled = true
app.Settings().RateLimits.Rules = []core.RateLimitRule{
{MaxRequests: 100, Label: "abc"},
{MaxRequests: 0, Label: "*:update"},
}
},
ExpectedStatus: 429,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
// dynamic body limit checks
// -----------------------------------------------------------
{
Name: "body > collection BodyLimit",
Method: http.MethodPatch,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
// the exact body doesn't matter as long as it returns 413
Body: bytes.NewReader(make([]byte, apis.DefaultMaxBodySize+5+20+2+1)),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
collection, err := app.FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
// adjust field sizes for the test
// ---
fileOneField := collection.Fields.GetByName("file_one").(*core.FileField)
fileOneField.MaxSize = 5
fileManyField := collection.Fields.GetByName("file_many").(*core.FileField)
fileManyField.MaxSize = 10
fileManyField.MaxSelect = 2
jsonField := collection.Fields.GetByName("json").(*core.JSONField)
jsonField.MaxSize = 2
err = app.Save(collection)
if err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 413,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "body <= collection BodyLimit",
Method: http.MethodPatch,
URL: "/v1/collections/demo1/records/imy661ixudk5izi",
// the exact body doesn't matter as long as it doesn't return 413
Body: bytes.NewReader(make([]byte, apis.DefaultMaxBodySize+5+20+2)),
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
collection, err := app.FindCollectionByNameOrId("demo1")
if err != nil {
t.Fatal(err)
}
// adjust field sizes for the test
// ---
fileOneField := collection.Fields.GetByName("file_one").(*core.FileField)
fileOneField.MaxSize = 5
fileManyField := collection.Fields.GetByName("file_many").(*core.FileField)
fileManyField.MaxSize = 10
fileManyField.MaxSelect = 2
jsonField := collection.Fields.GetByName("json").(*core.JSONField)
jsonField.MaxSize = 2
err = app.Save(collection)
if err != nil {
t.Fatal(err)
}
},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}