mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
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.
1722 lines
62 KiB
Go
1722 lines
62 KiB
Go
package apis_test
|
|
|
|
import (
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/tests"
|
|
"github.com/hanzoai/base/tools/list"
|
|
)
|
|
|
|
// collectionCount is how many collections a freshly bootstrapped test app has.
|
|
//
|
|
// It used to be written as the literal 27 in two assertions. That is a number
|
|
// every migration changes — it had already drifted to 31, and both scenarios
|
|
// had been failing ever since the calendar-booking collections landed. A stale
|
|
// literal in a passing-by-default position is a test that stops testing.
|
|
//
|
|
// Deriving it keeps the assertion that matters — the list endpoint reports
|
|
// EVERY collection, dropping or filtering none — without re-arming a trap that
|
|
// fires on the next migration.
|
|
func collectionCount(t *testing.T) int {
|
|
total, _ := collectionCounts(t)
|
|
return total
|
|
}
|
|
|
|
// collectionCounts reports how many collections a freshly bootstrapped test app
|
|
// has, and how many of those are NOT system collections.
|
|
//
|
|
// Callers need both because the numbers that used to be hardcoded were derived
|
|
// from each other by hand, in comments: "17 framework/system + demo collections
|
|
// plus the 10 CRM ... collections" for the total, and "9 original non-system
|
|
// collections + the 10 CRM ... collections are all missing from this import
|
|
// payload and get deleted" for the delete-event counts. Arithmetic over the
|
|
// migration list is wrong the moment a migration lands, and it was.
|
|
func collectionCounts(t *testing.T) (total, nonSystem int) {
|
|
t.Helper()
|
|
app, err := tests.NewTestApp()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer app.Cleanup()
|
|
|
|
var cols []*core.Collection
|
|
if err := app.CollectionQuery().All(&cols); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range cols {
|
|
if !c.System {
|
|
nonSystem++
|
|
}
|
|
}
|
|
return len(cols), nonSystem
|
|
}
|
|
|
|
func TestCollectionsList(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
totalItems := `"totalItems":` + strconv.Itoa(collectionCount(t))
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "unauthorized",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as regular user",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"page":1`,
|
|
`"perPage":30`,
|
|
totalItems,
|
|
`"items":[{`,
|
|
`"name":"` + core.CollectionNameSuperusers + `"`,
|
|
`"name":"users"`,
|
|
`"name":"nologin"`,
|
|
`"name":"clients"`,
|
|
`"name":"demo1"`,
|
|
`"name":"demo2"`,
|
|
`"name":"demo3"`,
|
|
`"name":"demo4"`,
|
|
`"name":"demo5"`,
|
|
`"name":"numeric_id_view"`,
|
|
`"name":"view1"`,
|
|
`"name":"view2"`,
|
|
`"type":"auth"`,
|
|
`"type":"base"`,
|
|
`"type":"view"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionsListRequest": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + paging and sorting",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections?page=2&perPage=2&sort=-created",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"page":2`,
|
|
`"perPage":2`,
|
|
totalItems,
|
|
`"items":[{`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionsListRequest": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + invalid filter",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections?filter=invalidfield~'demo2'",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + valid filter",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections?filter=name~'demo'",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"page":1`,
|
|
`"perPage":30`,
|
|
`"totalItems":5`,
|
|
`"items":[{`,
|
|
`"name":"demo1"`,
|
|
`"name":"demo2"`,
|
|
`"name":"demo3"`,
|
|
`"name":"demo4"`,
|
|
`"name":"demo5"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionsListRequest": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "OnCollectionsListRequest tx body write check",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.OnCollectionsListRequest().BindFunc(func(e *core.CollectionsListRequestEvent) 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{"OnCollectionsListRequest": 1},
|
|
ExpectedContent: []string{"TX_ERROR"},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|
|
|
|
func TestCollectionView(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "unauthorized",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/demo1",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as regular user",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/demo1",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + nonexisting collection identifier",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/missing",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 404,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + using the collection name",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/demo1",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"id":"wsmn24bux7wo113"`,
|
|
`"name":"demo1"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionViewRequest": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + using the collection id",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/wsmn24bux7wo113",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"id":"wsmn24bux7wo113"`,
|
|
`"name":"demo1"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionViewRequest": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "OnCollectionViewRequest tx body write check",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/wsmn24bux7wo113",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.OnCollectionViewRequest().BindFunc(func(e *core.CollectionRequestEvent) 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{"OnCollectionViewRequest": 1},
|
|
ExpectedContent: []string{"TX_ERROR"},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|
|
|
|
func TestCollectionDelete(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ensureDeletedFiles := func(app *tests.TestApp, collectionId string) {
|
|
// the files go after the delete commits, in the background
|
|
app.WaitFileDeletes()
|
|
|
|
storageDir := filepath.Join(app.DataDir(), "storage", collectionId)
|
|
|
|
entries, _ := os.ReadDir(storageDir)
|
|
if len(entries) != 0 {
|
|
t.Errorf("Expected empty/deleted dir, found %d", len(entries))
|
|
}
|
|
}
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "unauthorized",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/demo1",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as regular user",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/demo1",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + nonexisting collection identifier",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/missing",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 404,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + using the collection name",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/demo5",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 204,
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionDeleteRequest": 1,
|
|
"OnCollectionDelete": 1,
|
|
"OnCollectionDeleteExecute": 1,
|
|
"OnCollectionAfterDeleteSuccess": 1,
|
|
"OnModelDelete": 1,
|
|
"OnModelDeleteExecute": 1,
|
|
"OnModelAfterDeleteSuccess": 1,
|
|
},
|
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
|
ensureDeletedFiles(app, "9n89pl5vkct6330")
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + using the collection id",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/9n89pl5vkct6330",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 204,
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionDeleteRequest": 1,
|
|
"OnCollectionDelete": 1,
|
|
"OnCollectionDeleteExecute": 1,
|
|
"OnCollectionAfterDeleteSuccess": 1,
|
|
"OnModelDelete": 1,
|
|
"OnModelDeleteExecute": 1,
|
|
"OnModelAfterDeleteSuccess": 1,
|
|
},
|
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
|
ensureDeletedFiles(app, "9n89pl5vkct6330")
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + trying to delete a system collection",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/" + core.CollectionNameSuperusers,
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionDeleteRequest": 1,
|
|
"OnCollectionDelete": 1,
|
|
"OnCollectionDeleteExecute": 1,
|
|
"OnCollectionAfterDeleteError": 1,
|
|
"OnModelDelete": 1,
|
|
"OnModelDeleteExecute": 1,
|
|
"OnModelAfterDeleteError": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + trying to delete a referenced collection",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/demo2",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionDeleteRequest": 1,
|
|
"OnCollectionDelete": 1,
|
|
"OnCollectionDeleteExecute": 1,
|
|
"OnCollectionAfterDeleteError": 1,
|
|
"OnModelDelete": 1,
|
|
"OnModelDeleteExecute": 1,
|
|
"OnModelAfterDeleteError": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + deleting a view",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/view2",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 204,
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionDeleteRequest": 1,
|
|
"OnCollectionDelete": 1,
|
|
"OnCollectionDeleteExecute": 1,
|
|
"OnCollectionAfterDeleteSuccess": 1,
|
|
"OnModelDelete": 1,
|
|
"OnModelDeleteExecute": 1,
|
|
"OnModelAfterDeleteSuccess": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "OnCollectionDeleteRequest tx body write check",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/view2",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.OnCollectionDeleteRequest().BindFunc(func(e *core.CollectionRequestEvent) 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{"OnCollectionDeleteRequest": 1},
|
|
ExpectedContent: []string{"TX_ERROR"},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|
|
|
|
func TestCollectionCreate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "unauthorized",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as regular user",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{"name":"new","type":"base","fields":[{"type":"text","name":"test"}]}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + empty data",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(``),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"name":{"code":"validation_required"`,
|
|
},
|
|
NotExpectedContent: []string{
|
|
`"fields":{`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionAfterCreateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelAfterCreateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + invalid data (eg. existing name)",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{"name":"demo1","type":"base","fields":[{"type":"text","name":""}]}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"fields":{`,
|
|
`"name":{"code":"validation_collection_name_exists"`,
|
|
`"name":{"code":"validation_required"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionAfterCreateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelAfterCreateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + valid data",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{"name":"new","type":"base","fields":[{"type":"text","id":"12345789","name":"test"}]}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"id":`,
|
|
`"name":"new"`,
|
|
`"type":"base"`,
|
|
`"system":false`,
|
|
// ensures that id field was prepended
|
|
`"fields":[{"autogeneratePattern":"[a-z0-9]{15}","hidden":false,"id":"text3208210256","max":36,"min":15,"name":"id","pattern":"^[a-z0-9\\-]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"},{"autogeneratePattern":"","hidden":false,"id":"12345789","max":0,"min":0,"name":"test","pattern":"","presentable":false,"primaryKey":false,"required":false,"system":false,"type":"text"}]`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionCreateExecute": 1,
|
|
"OnCollectionAfterCreateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelCreateExecute": 1,
|
|
"OnModelAfterCreateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "creating auth collection (default settings merge test)",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"auth",
|
|
"emailChangeToken":{"duration":123},
|
|
"fields":[
|
|
{"type":"text","id":"12345789","name":"test"},
|
|
{"type":"text","name":"tokenKey","system":true,"required":false,"min":10}
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"id":`,
|
|
`"name":"new"`,
|
|
`"type":"auth"`,
|
|
`"system":false`,
|
|
`"authRule":""`,
|
|
`"manageRule":null`,
|
|
`"name":"test"`,
|
|
`"name":"id"`,
|
|
`"name":"tokenKey"`,
|
|
`"name":"email"`,
|
|
`"name":"emailVisibility"`,
|
|
`"name":"verified"`,
|
|
`"duration":123`,
|
|
// should overwrite the user required option but keep the min value
|
|
`{"autogeneratePattern":"","hidden":true,"id":"text2504183744","max":0,"min":10,"name":"tokenKey","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":true,"type":"text"}`,
|
|
},
|
|
NotExpectedContent: []string{
|
|
`"secret":"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionCreateExecute": 1,
|
|
"OnCollectionAfterCreateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelCreateExecute": 1,
|
|
"OnModelAfterCreateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "creating base collection with reserved auth fields",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"base",
|
|
"fields":[
|
|
{"type":"text","name":"email"},
|
|
{"type":"text","name":"username"},
|
|
{"type":"text","name":"verified"},
|
|
{"type":"text","name":"emailVisibility"},
|
|
{"type":"text","name":"lastResetSentAt"},
|
|
{"type":"text","name":"lastVerificationSentAt"},
|
|
{"type":"text","name":"tokenKey"},
|
|
{"type":"text","name":"passwordHash"},
|
|
{"type":"text","name":"password"},
|
|
{"type":"text","name":"passwordConfirm"},
|
|
{"type":"text","name":"oldPassword"}
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"name":"new"`,
|
|
`"type":"base"`,
|
|
`"fields":[{`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionCreateExecute": 1,
|
|
"OnCollectionAfterCreateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelCreateExecute": 1,
|
|
"OnModelAfterCreateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "trying to create base collection with reserved system fields",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"base",
|
|
"fields":[
|
|
{"type":"text","name":"id"},
|
|
{"type":"text","name":"expand"},
|
|
{"type":"text","name":"collectionId"},
|
|
{"type":"text","name":"collectionName"}
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{"fields":{`,
|
|
`"1":{"name":{"code":"validation_not_in_invalid`,
|
|
`"2":{"name":{"code":"validation_not_in_invalid`,
|
|
`"3":{"name":{"code":"validation_not_in_invalid`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionAfterCreateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelAfterCreateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "OnCollectionCreateRequest tx body write check",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{"name":"new","type":"base"}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.OnCollectionCreateRequest().BindFunc(func(e *core.CollectionRequestEvent) 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{"OnCollectionCreateRequest": 1},
|
|
ExpectedContent: []string{"TX_ERROR"},
|
|
},
|
|
|
|
// view
|
|
// -----------------------------------------------------------
|
|
{
|
|
Name: "trying to create view collection with invalid options",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"view",
|
|
"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
|
|
"viewQuery":"invalid"
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"viewQuery":{"code":"validation_invalid_view_query`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionAfterCreateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelAfterCreateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "creating view collection",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"view",
|
|
"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
|
|
"viewQuery": "select 1 as id from ` + core.CollectionNameSuperusers + `"
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"name":"new"`,
|
|
`"type":"view"`,
|
|
`"fields":[{"autogeneratePattern":"","hidden":false,"id":"text3208210256","max":0,"min":0,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"}]`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionCreateExecute": 1,
|
|
"OnCollectionAfterCreateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelCreateExecute": 1,
|
|
"OnModelAfterCreateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
|
|
// indexes
|
|
// -----------------------------------------------------------
|
|
{
|
|
Name: "creating base collection with invalid indexes",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"base",
|
|
"fields":[
|
|
{"type":"text","name":"test"}
|
|
],
|
|
"indexes": [
|
|
"create index idx_test1 on new (test)",
|
|
"create index idx_test2 on new (missing)"
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"indexes":{"1":{"code":"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionCreateExecute": 1,
|
|
"OnCollectionAfterCreateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelCreateExecute": 1,
|
|
"OnModelAfterCreateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "creating base collection with index name from another collection",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"base",
|
|
"fields":[
|
|
{"type":"text","name":"test"}
|
|
],
|
|
"indexes": [
|
|
"create index exist_test on new (test)"
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
demo1, err := app.FindCollectionByNameOrId("demo1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
demo1.AddIndex("exist_test", false, "updated", "")
|
|
if err = app.Save(demo1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"indexes":{`,
|
|
`"0":{"code":"validation_existing_index_name"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionAfterCreateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelAfterCreateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "creating base collection with 2 indexes using the same name",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"base",
|
|
"indexes": [
|
|
"create index duplicate_idx on new (created)",
|
|
"create index duplicate_idx on new (updated)"
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"indexes":{`,
|
|
`"1":{"code":"validation_duplicated_index_name"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionAfterCreateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelAfterCreateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "creating base collection with valid indexes (+ random table name)",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"type":"base",
|
|
"fields":[
|
|
{"type":"text","name":"test"}
|
|
],
|
|
"indexes": [
|
|
"create index idx_test1 on new (test)",
|
|
"create index idx_test2 on anything (id, test)"
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"name":"new"`,
|
|
`"type":"base"`,
|
|
`"indexes":[`,
|
|
`idx_test1`,
|
|
`idx_test2`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionCreateRequest": 1,
|
|
"OnCollectionCreate": 1,
|
|
"OnCollectionCreateExecute": 1,
|
|
"OnCollectionAfterCreateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelCreate": 1,
|
|
"OnModelCreateExecute": 1,
|
|
"OnModelAfterCreateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
|
indexes, err := app.TableIndexes("new")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := []string{"idx_test1", "idx_test2"}
|
|
if len(indexes) != len(expected) {
|
|
t.Fatalf("Expected %d indexes, got %d\n%v", len(expected), len(indexes), indexes)
|
|
}
|
|
for name := range indexes {
|
|
if !list.ExistInSlice(name, expected) {
|
|
t.Fatalf("Missing index %q", name)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|
|
|
|
func TestCollectionUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "unauthorized",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo1",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as regular user",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo1",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + missing collection",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/missing",
|
|
Body: strings.NewReader(`{}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 404,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + empty body",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo1",
|
|
Body: strings.NewReader(`{}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"id":"wsmn24bux7wo113"`,
|
|
`"name":"demo1"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionUpdateExecute": 1,
|
|
"OnCollectionAfterUpdateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelUpdateExecute": 1,
|
|
"OnModelAfterUpdateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "OnCollectionUpdateRequest tx body write check",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo1",
|
|
Body: strings.NewReader(`{}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
app.OnCollectionUpdateRequest().BindFunc(func(e *core.CollectionRequestEvent) 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{"OnCollectionUpdateRequest": 1},
|
|
ExpectedContent: []string{"TX_ERROR"},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + invalid data (eg. existing name)",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo1",
|
|
Body: strings.NewReader(`{
|
|
"name":"demo2",
|
|
"type":"auth"
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"name":{"code":"validation_collection_name_exists"`,
|
|
`"type":{"code":"validation_collection_type_change"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionAfterUpdateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelAfterUpdateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser + valid data",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo1",
|
|
Body: strings.NewReader(`{"name":"new"}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"id":`,
|
|
`"name":"new"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionUpdateExecute": 1,
|
|
"OnCollectionAfterUpdateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelUpdateExecute": 1,
|
|
"OnModelAfterUpdateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
|
// check if the record table was renamed
|
|
if !app.HasTable("new") {
|
|
t.Fatal("Couldn't find record table 'new'.")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "trying to update collection with reserved fields",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo1",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"fields":[
|
|
{"type":"text","name":"id","id":"_hzf_text_id_","system":true,"min":15,"max":15,"pattern":"^[a-z0-9]+$","autogeneratePattern":"[a-z0-9]{15}","required":true,"primaryKey":true},
|
|
{"type":"text","name":"text","id":"u7spsiph"},
|
|
{"type":"bool","name":"bool","id":"puk534el"},
|
|
{"type":"autodate","name":"created","id":"_hzf_autodate_created_","onCreate":true,"onUpdate":false},
|
|
{"type":"autodate","name":"updated","id":"_hzf_autodate_updated_","onCreate":true,"onUpdate":true},
|
|
{"type":"text","name":"expand"},
|
|
{"type":"text","name":"collectionId"},
|
|
{"type":"text","name":"collectionName"}
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{"fields":{`,
|
|
`"5":{"name":{"code":"validation_not_in_invalid`,
|
|
`"6":{"name":{"code":"validation_not_in_invalid`,
|
|
`"7":{"name":{"code":"validation_not_in_invalid`,
|
|
},
|
|
NotExpectedContent: []string{
|
|
`"0":`,
|
|
`"1":`,
|
|
`"2":`,
|
|
`"3":`,
|
|
`"4":`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionAfterUpdateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelAfterUpdateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "trying to update collection with changed/removed system fields",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo1",
|
|
Body: strings.NewReader(`{
|
|
"name":"new",
|
|
"fields":[
|
|
{"type":"text","name":"created"}
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{"fields":{`,
|
|
`"code":"validation_system_field_change"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionAfterUpdateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelAfterUpdateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "trying to update auth collection with invalid options",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/users",
|
|
Body: strings.NewReader(`{
|
|
"oauth2":{"enabled":true,"providers":[{"name":"missing"}]}
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"oauth2":{`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionAfterUpdateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelAfterUpdateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
|
|
// view
|
|
// -----------------------------------------------------------
|
|
{
|
|
Name: "trying to update view collection with invalid options",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/view1",
|
|
Body: strings.NewReader(`{
|
|
"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
|
|
"viewQuery":"invalid"
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"viewQuery":{"code":"validation_invalid_view_query"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionAfterUpdateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelAfterUpdateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "updating view collection",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/view2",
|
|
Body: strings.NewReader(`{
|
|
"name":"view2_update",
|
|
"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
|
|
"viewQuery": "select 2 as id, created, updated, email from ` + core.CollectionNameSuperusers + `"
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"name":"view2_update"`,
|
|
`"type":"view"`,
|
|
`"fields":[{`,
|
|
`"name":"email"`,
|
|
`"name":"id"`,
|
|
`"name":"created"`,
|
|
`"name":"updated"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionUpdateExecute": 1,
|
|
"OnCollectionAfterUpdateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelUpdateExecute": 1,
|
|
"OnModelAfterUpdateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
|
|
// indexes
|
|
// -----------------------------------------------------------
|
|
{
|
|
Name: "updating base collection with invalid indexes",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo2",
|
|
Body: strings.NewReader(`{
|
|
"indexes": [
|
|
"create unique idx_test1 on demo1 (text)",
|
|
"create index idx_test2 on demo2 (id, title)"
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"indexes":{"0":{"code":"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionAfterUpdateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelAfterUpdateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
|
|
{
|
|
Name: "updating base collection with index name from another collection",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo2",
|
|
Body: strings.NewReader(`{
|
|
"indexes": [
|
|
"create index exist_test on new (test)"
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
demo1, err := app.FindCollectionByNameOrId("demo1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
demo1.AddIndex("exist_test", false, "updated", "")
|
|
if err = app.Save(demo1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"indexes":{`,
|
|
`"0":{"code":"validation_existing_index_name"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionAfterUpdateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelAfterUpdateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
{
|
|
Name: "updating base collection with 2 indexes using the same name",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo2",
|
|
Body: strings.NewReader(`{
|
|
"indexes": [
|
|
"create index duplicate_idx on new (created)",
|
|
"create index duplicate_idx on new (updated)"
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{
|
|
`"data":{`,
|
|
`"indexes":{`,
|
|
`"1":{"code":"validation_duplicated_index_name"`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionAfterUpdateError": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelAfterUpdateError": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
},
|
|
|
|
{
|
|
Name: "updating base collection with valid indexes (+ random table name)",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/demo2",
|
|
Body: strings.NewReader(`{
|
|
"indexes": [
|
|
"create unique index idx_test1 on demo2 (title)",
|
|
"create index idx_test2 on anything (active)"
|
|
]
|
|
}`),
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"name":"demo2"`,
|
|
`"indexes":[`,
|
|
`idx_test1`,
|
|
`idx_test2`,
|
|
},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionUpdateExecute": 1,
|
|
"OnCollectionAfterUpdateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelUpdateExecute": 1,
|
|
"OnModelAfterUpdateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
|
indexes, err := app.TableIndexes("demo2")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := []string{"idx_test1", "idx_test2"}
|
|
if len(indexes) != len(expected) {
|
|
t.Fatalf("Expected %d indexes, got %d\n%v", len(expected), len(indexes), indexes)
|
|
}
|
|
for name := range indexes {
|
|
if !list.ExistInSlice(name, expected) {
|
|
t.Fatalf("Missing index %q", name)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|
|
|
|
func TestCollectionScaffolds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "unauthorized",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/meta/scaffolds",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as regular user",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/meta/scaffolds",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser",
|
|
Method: http.MethodGet,
|
|
URL: "/v1/collections/meta/scaffolds",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{
|
|
`"id":""`,
|
|
`"name":""`,
|
|
`"auth":{`,
|
|
`"base":{`,
|
|
`"view":{`,
|
|
`"type":"auth"`,
|
|
`"type":"base"`,
|
|
`"type":"view"`,
|
|
`"fields":[{`,
|
|
`"fields":[{`,
|
|
`"id":"text3208210256"`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|
|
|
|
// A token secret signs JWTs Base mints, so the update door does not take one
|
|
// from a request body — the rest of the same token config still applies — and
|
|
// rotation is the verb that replaces it.
|
|
func TestCollectionTokenSecretsAreTheServersToMint(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const superuserToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio"
|
|
const userToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo"
|
|
|
|
secrets := func(t testing.TB, app *tests.TestApp) map[string]string {
|
|
c, err := app.FindCollectionByNameOrId("users")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return map[string]string{
|
|
"authToken": c.AuthToken.Secret,
|
|
"fileToken": c.FileToken.Secret,
|
|
"emailChangeToken": c.EmailChangeToken.Secret,
|
|
"verificationToken": c.VerificationToken.Secret,
|
|
}
|
|
}
|
|
|
|
var stated map[string]string
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "update naming a secret keeps the stored one and applies the rest",
|
|
Method: http.MethodPatch,
|
|
URL: "/v1/collections/users",
|
|
Body: strings.NewReader(`{
|
|
"authToken":{"secret":"` + strings.Repeat("z", 50) + `","duration":1234}
|
|
}`),
|
|
Headers: map[string]string{"Authorization": superuserToken},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{`"duration":1234`},
|
|
NotExpectedContent: []string{`"secret":"`},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionUpdateExecute": 1,
|
|
"OnCollectionAfterUpdateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelUpdateExecute": 1,
|
|
"OnModelAfterUpdateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
stated = secrets(t, app)
|
|
},
|
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
|
for name, secret := range secrets(t, app) {
|
|
if secret != stated[name] {
|
|
t.Fatalf("Expected the stored %s secret to be untouched, got %q", name, secret)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
Name: "rotate unauthorized",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/rotate",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "rotate as regular user",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/rotate",
|
|
Headers: map[string]string{"Authorization": userToken},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "rotate a collection that issues no tokens",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/demo1/rotate",
|
|
Headers: map[string]string{"Authorization": superuserToken},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "rotate as superuser",
|
|
Method: http.MethodPost,
|
|
URL: "/v1/collections/users/rotate",
|
|
Headers: map[string]string{"Authorization": superuserToken},
|
|
ExpectedStatus: 200,
|
|
ExpectedContent: []string{`"name":"users"`},
|
|
NotExpectedContent: []string{`"secret":"`},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnCollectionUpdateRequest": 1,
|
|
"OnCollectionUpdate": 1,
|
|
"OnCollectionUpdateExecute": 1,
|
|
"OnCollectionAfterUpdateSuccess": 1,
|
|
"OnCollectionValidate": 1,
|
|
"OnModelUpdate": 1,
|
|
"OnModelUpdateExecute": 1,
|
|
"OnModelAfterUpdateSuccess": 1,
|
|
"OnModelValidate": 1,
|
|
},
|
|
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
|
stated = secrets(t, app)
|
|
},
|
|
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
|
for name, secret := range secrets(t, app) {
|
|
if secret == stated[name] {
|
|
t.Fatalf("Expected a fresh %s secret", name)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|
|
|
|
func TestCollectionTruncate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
scenarios := []tests.ApiScenario{
|
|
{
|
|
Name: "unauthorized",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/demo5/truncate",
|
|
ExpectedStatus: 401,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as regular user",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/demo5/truncate",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJfdXNlcnNfYXV0aF8iLCJleHAiOjI1MjQ2MDQ0NjEsImlkIjoiNHExeGxjbG1mbG9rdTMzIiwicmVmcmVzaGFibGUiOnRydWUsInR5cGUiOiJhdXRoIn0.AuFTIzCsdLEy-5adFzpjZzbqAdTP6Iu9B1wPBAxLBgo",
|
|
},
|
|
ExpectedStatus: 403,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
{
|
|
Name: "authorized as superuser",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/demo5/truncate",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 204,
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnModelDelete": 2,
|
|
"OnModelDeleteExecute": 2,
|
|
"OnModelAfterDeleteSuccess": 2,
|
|
"OnRecordDelete": 2,
|
|
"OnRecordDeleteExecute": 2,
|
|
"OnRecordAfterDeleteSuccess": 2,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser but collection with required cascade delete references",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/demo3/truncate",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{
|
|
"*": 0,
|
|
"OnModelDelete": 2,
|
|
"OnModelDeleteExecute": 2,
|
|
"OnModelAfterDeleteError": 2,
|
|
"OnModelUpdate": 2,
|
|
"OnModelUpdateExecute": 2,
|
|
"OnModelAfterUpdateError": 2,
|
|
"OnRecordDelete": 2,
|
|
"OnRecordDeleteExecute": 2,
|
|
"OnRecordAfterDeleteError": 2,
|
|
"OnRecordUpdate": 2,
|
|
"OnRecordUpdateExecute": 2,
|
|
"OnRecordAfterUpdateError": 2,
|
|
},
|
|
},
|
|
{
|
|
Name: "authorized as superuser trying to truncate view collection",
|
|
Method: http.MethodDelete,
|
|
URL: "/v1/collections/view2/truncate",
|
|
Headers: map[string]string{
|
|
"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb2xsZWN0aW9uSWQiOiJoYmNfMzE0MjYzNTgyMyIsImV4cCI6MjUyNDYwNDQ2MSwiaWQiOiJzeXdiaGVjbmg0NnJobTAiLCJyZWZyZXNoYWJsZSI6dHJ1ZSwidHlwZSI6ImF1dGgifQ.CXBf8BazmUeg2RnJW8OEs1UFYF41rbCMOa6YZa4wZio",
|
|
},
|
|
ExpectedStatus: 400,
|
|
ExpectedContent: []string{`"data":{}`},
|
|
ExpectedEvents: map[string]int{"*": 0},
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
scenario.Test(t)
|
|
}
|
|
}
|