Files
base/apis/record_crud_test.go
zeekay 6a3066b08e Test apps open a migrated fixture, so core and apis fit the runner
tests/data lacked twelve migrations, and every test app ran them before
its test began: about four seconds an app, which put core and apis past
go test's ten minutes on the runner, and past thirty once the timeout
was raised. The fixture now holds them (go run ./tests/fixture writes
it), and TestTheFixtureIsMigrated fails, naming the migration, while
one is missing. On one host core went from 267 s to 26 s, apis from
432 s to 23 s, and the whole suite runs in 51 s.

The delete tests gave a background goroutine 100 ms to remove a
record's files, which a slow runner did not always meet. BaseApp
counts those deletions and WaitFileDeletes waits for them: the tests
wait on the count instead of the clock, and ResetBootstrapState waits
too, so a Base that stops no longer drops a deletion in flight.

TestSQLRun expected a CREATE TABLE to report one affected row. SQLite
answers a statement that writes no rows with the connection's last
count, and that row was the last migration's _migrations insert; the
handler now reads 0, the number it changed.

go test runs with its default timeout again, and crm_test.go is gofmt'd.
2026-10-19 09:08:39 -07:00

3356 lines
112 KiB
Go

package apis_test
import (
"bytes"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"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) {
// the files go after the delete commits, in the background
app.WaitFileDeletes()
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",
},
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",
},
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)
}
}