snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

2
server/tests/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
workflows/var

View File

@@ -0,0 +1,244 @@
package apigw
import (
"context"
"errors"
"fmt"
"os"
"path"
"testing"
"github.com/cortezaproject/corteza/server/pkg/apigw/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers/sqlite"
"github.com/cortezaproject/corteza/server/app"
as "github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/pkg/api/server"
"github.com/cortezaproject/corteza/server/pkg/apigw"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/csv"
"github.com/cortezaproject/corteza/server/pkg/envoy/directory"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
es "github.com/cortezaproject/corteza/server/pkg/envoy/store"
"github.com/cortezaproject/corteza/server/pkg/envoy/yaml"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/label"
ltype "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/rest"
"github.com/cortezaproject/corteza/server/system/service"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/go-chi/chi/v5"
_ "github.com/joho/godotenv/autoload"
"github.com/steinfletcher/apitest"
"github.com/stretchr/testify/require"
)
type (
helper struct {
t *testing.T
a *require.Assertions
cUser *sysTypes.User
roleID uint64
token []byte
}
)
var (
testApp *app.CortezaApp
r chi.Router
eventBus = eventbus.New()
)
func init() {
helpers.RecursiveDotEnvLoad()
}
func InitTestApp() {
ctx := cli.Context()
if testApp == nil {
testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) {
service.DefaultStore, err = sqlite.ConnectInMemory(ctx)
if err != nil {
return err
}
eventbus.Set(eventBus)
return nil
})
}
if err := testApp.Activate(ctx); err != nil {
panic(fmt.Errorf("could not activate corteza: %v", err))
}
if r == nil {
r = chi.NewRouter()
r.Use(server.BaseMiddleware(false, logger.Default())...)
helpers.BindAuthMiddleware(r)
// Sys routes for route management tests
r.Group(rest.MountRoutes())
// API gw routes
apigw.Setup(types.Config{Enabled: true, Profiler: struct {
Enabled bool
Global bool
}{Enabled: true, Global: true}}, service.DefaultLogger, service.DefaultStore)
err := apigw.Service().Reload(ctx)
if err != nil {
panic(err)
}
r.Handle("/*", apigw.Service())
}
}
func TestMain(m *testing.M) {
InitTestApp()
os.Exit(m.Run())
}
func newHelper(t *testing.T) helper {
h := helper{
t: t,
a: require.New(t),
roleID: id.Next(),
cUser: &sysTypes.User{
ID: id.Next(),
},
}
h.cUser.SetRoles(h.roleID)
helpers.UpdateRBAC(h.roleID)
var err error
ctx := context.Background()
h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser))
if err != nil {
panic(err)
}
return h
}
func (h helper) MyRole() uint64 {
return h.roleID
}
// Returns context w/ security details
func (h helper) secCtx() context.Context {
return auth.SetIdentityToContext(context.Background(), h.cUser)
}
// apitest basics, initialize, set handler, add auth
func (h helper) apiInit() *apitest.APITest {
InitTestApp()
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderRawAuthBearer(h.token))
}
func setupScenario(t *testing.T) (context.Context, helper, store.Storer) {
ctx, h, s := setup(t)
loadScenario(ctx, s, t, h)
loadRbacRules(ctx, s, t, h)
_ = apigw.Service().Reload(ctx)
return ctx, h, s
}
func loadRbacRules(ctx context.Context, s store.Storer, t *testing.T, h helper) {
helpers.AllowMeWorkflowSearch(h)
}
func setup(t *testing.T) (context.Context, helper, store.Storer) {
h := newHelper(t)
s := service.DefaultStore
u := &sysTypes.User{
ID: id.Next(),
}
u.SetRoles(auth.BypassRoles().IDs()...)
ctx := auth.SetIdentityToContext(context.Background(), u)
return ctx, h, s
}
// Unwraps error before it passes it to the tester
func (h helper) noError(err error) {
for errors.Unwrap(err) != nil {
err = errors.Unwrap(err)
}
h.a.NoError(err)
}
func (h helper) setLabel(res label.LabeledResource, name, value string) {
h.a.NoError(store.UpsertLabel(h.secCtx(), service.DefaultStore, &ltype.Label{
Kind: res.LabelResourceKind(),
ResourceID: res.LabelResourceID(),
Name: name,
Value: ltype.LabelValue{Val: value},
}))
}
func loadScenario(ctx context.Context, s store.Storer, t *testing.T, h helper) {
loadScenarioWithName(ctx, s, t, h, t.Name()[5:])
}
func loadScenarioWithName(ctx context.Context, s store.Storer, t *testing.T, h helper, scenario string) {
cleanup(ctx, h, s)
parseEnvoy(ctx, s, h, path.Join("testdata", scenario))
loadWorkflows(ctx, h)
}
func loadWorkflows(ctx context.Context, h helper) {
err := as.DefaultWorkflow.Load(ctx)
h.a.NoError(err)
}
func cleanup(ctx context.Context, h helper, s store.Storer) {
h.noError(s.TruncateApigwFilters(ctx))
h.noError(s.TruncateApigwRoutes(ctx))
h.noError(s.TruncateRbacRules(ctx))
h.noError(s.TruncateAutomationTriggers(ctx))
h.noError(s.TruncateAutomationWorkflows(ctx))
}
func parseEnvoy(ctx context.Context, s store.Storer, h helper, path string) {
nn, err := directory.Decode(
ctx,
path,
yaml.Decoder(),
csv.Decoder(),
)
if err != nil {
h.t.Fatalf("failed to decode scenario data: %v", err)
}
crs := resource.ComposeRecordShaper()
nn, err = resource.Shape(nn, crs)
h.a.NoError(err)
// import into the store
se := es.NewStoreEncoder(s, dal.Service(), nil)
bld := envoy.NewBuilder(se)
g, err := bld.Build(ctx, nn...)
h.a.NoError(err)
err = envoy.Encode(ctx, g, se)
h.a.NoError(err)
}

View File

@@ -0,0 +1,20 @@
package apigw
import (
"net/http"
"testing"
)
func Test_postfilter_string_json(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
h.apiInit().
Get("/json/string").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Body("{\"baz\":{\"@value\":{\"1\":{\"@value\":1,\"@type\":\"Float\"},\"2\":{\"@value\":2,\"@type\":\"Float\"}},\"@type\":\"Vars\"},\"foo\":{\"@value\":\"bar\",\"@type\":\"String\"}}").
End()
}

View File

@@ -0,0 +1,20 @@
package apigw
import (
"net/http"
"testing"
)
func Test_postfilter_string_kv(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
h.apiInit().
Get("/json/kv").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Body("{\"baz\":\"123\",\"foo\":\"bar\"}").
End()
}

View File

@@ -0,0 +1,28 @@
package apigw
import (
"net/http"
"testing"
)
func Test_prefilter_header_failing(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
h.apiInit().
Get("/test").
Header("Accept", "application/json").
Header("Token", "brute-force-guess").
Expect(t).
Status(http.StatusBadRequest).
End()
h.apiInit().
Get("/test-hyphen").
Header("Accept-Language", "fr-CH, fr;q=0.9").
Header("Token", "brute-force-guess").
Expect(t).
Status(http.StatusBadRequest).
End()
}

View File

@@ -0,0 +1,46 @@
package apigw
import (
"net/http"
"testing"
)
func Test_prefilter_header_n_routes(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
// First (a) route query validation
h.apiInit().
Get("/a").
Header("P", "a").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
End()
h.apiInit().
Get("/a").
Header("P", "b").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusBadRequest).
End()
// Second (b) route query validation
h.apiInit().
Get("/b").
Header("P", "b").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
End()
h.apiInit().
Get("/b").
Header("P", "a").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusBadRequest).
End()
}

View File

@@ -0,0 +1,28 @@
package apigw
import (
"net/http"
"testing"
)
func Test_prefilter_header_passing(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
h.apiInit().
Get("/test").
Header("Accept", "application/json").
Header("Token", "super-secret-token").
Expect(t).
Status(http.StatusOK).
End()
h.apiInit().
Get("/test-hyphen").
Header("Accept-Language", "fr-CH, fr;q=0.9").
Header("Token", "super-secret-token").
Expect(t).
Status(http.StatusOK).
End()
}

View File

@@ -0,0 +1,28 @@
package apigw
import (
"net/http"
"testing"
)
func Test_prefilter_query_failing(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
h.apiInit().
Get("/test").
Query("token", "brute-force-guess").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusBadRequest).
End()
h.apiInit().
Get("/test-hyphen").
Query("foo-bar", "encrypted-string").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusBadRequest).
End()
}

View File

@@ -0,0 +1,46 @@
package apigw
import (
"net/http"
"testing"
)
func Test_prefilter_query_n_routes(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
// First (a) route query validation
h.apiInit().
Get("/a").
Query("p", "a").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
End()
h.apiInit().
Get("/a").
Query("p", "b").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusBadRequest).
End()
// Second (b) route query validation
h.apiInit().
Get("/b").
Query("p", "b").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
End()
h.apiInit().
Get("/b").
Query("p", "a").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusBadRequest).
End()
}

View File

@@ -0,0 +1,28 @@
package apigw
import (
"net/http"
"testing"
)
func Test_prefilter_query_passing(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
h.apiInit().
Get("/test").
Query("token", "super-secret-token").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
End()
h.apiInit().
Get("/test-hyphen").
Query("foo-bar", "encrypted-string").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
End()
}

View File

@@ -0,0 +1,21 @@
package apigw
import (
"net/http"
"testing"
)
func Test_processor_payload_simple(t *testing.T) {
var (
_, h, _ = setupScenario(t)
)
h.apiInit().
Get("/test").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Body("60").
End()
}

View File

@@ -0,0 +1,65 @@
package apigw
import (
"fmt"
"net/http"
"testing"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
)
func Test_route_reload(t *testing.T) {
var (
ctx, h, s = setupScenario(t)
)
r, err := store.LookupApigwRouteByEndpoint(ctx, s, "/test")
h.a.NoError(err)
h.apiInit().
Get("/test").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Body("60").
End()
helpers.AllowMe(h, r.RbacResource(), "update")
h.apiInit().
Put(fmt.Sprintf("/apigw/route/%d", r.ID)).
Header("Accept", "application/json").
FormData("endpoint", "/test").
FormData("method", "GET").
FormData("enabled", "false").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
h.apiInit().
Get("/test").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusNotFound).
End()
h.apiInit().
Put(fmt.Sprintf("/apigw/route/%d", r.ID)).
Header("Accept", "application/json").
FormData("endpoint", "/test").
FormData("method", "GET").
FormData("enabled", "true").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
h.apiInit().
Get("/test").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Body("60").
End()
}

View File

@@ -0,0 +1,38 @@
apigateway:
- endpoint: /json/string
method: GET
enabled: true
filters:
- ref: response
kind: postfilter
enabled: true
params:
input:
expr: toJSON(test)
type: String
header:
Content-Type: [ application/json ]
- ref: workflow
kind: processer
enabled: true
params:
workflow: 'test_variable_json'
workflows:
- id: 1
enabled: true
trace: false
handle: test_variable_json
meta:
name: Test variable
triggers:
- enabled: true
stepID: 1
steps:
- stepID: 1
kind: expressions
arguments:
- target: test
type: Any
expr: |
{"foo":"bar","baz":{"1":1, "2":2}}

View File

@@ -0,0 +1,38 @@
apigateway:
- endpoint: /json/kv
method: GET
enabled: true
filters:
- ref: response
kind: postfilter
enabled: true
params:
input:
expr: test
type: KV
header:
Content-Type: [ application/json ]
- ref: workflow
kind: processer
enabled: true
params:
workflow: 'test_variable_kv'
workflows:
- id: 1
enabled: true
trace: false
handle: test_variable_kv
meta:
name: Test variable
triggers:
- enabled: true
stepID: 1
steps:
- stepID: 1
kind: expressions
arguments:
- target: test
type: KV
expr: |
{"foo":"bar","baz":123}

View File

@@ -0,0 +1,20 @@
apigateway:
- endpoint: /test
method: GET
enabled: true
filters:
- ref: "header"
kind: "prefilter"
enabled: true
params:
expr: "Token == \"super-secret-token\""
- endpoint: /test-hyphen
method: GET
enabled: true
filters:
- ref: "header"
kind: "prefilter"
enabled: true
params:
expr: "Accept-Language == \"fr-CH, fr;q=0.9\""

View File

@@ -0,0 +1,20 @@
apigateway:
- endpoint: /a
method: GET
enabled: true
filters:
- ref: "header"
kind: "prefilter"
enabled: true
params:
expr: "P == \"a\""
- endpoint: /b
method: GET
enabled: true
filters:
- ref: "header"
kind: "prefilter"
enabled: true
params:
expr: "P == \"b\""

View File

@@ -0,0 +1,20 @@
apigateway:
- endpoint: /test
method: GET
enabled: true
filters:
- ref: "header"
kind: "prefilter"
enabled: true
params:
expr: "Token == \"super-secret-token\""
- endpoint: /test-hyphen
method: GET
enabled: true
filters:
- ref: "header"
kind: "prefilter"
enabled: true
params:
expr: "headers[\"Accept-Language\"] == \"fr-CH, fr;q=0.9\""

View File

@@ -0,0 +1,21 @@
apigateway:
- endpoint: /test
method: GET
enabled: true
filters:
- ref: "queryParam"
kind: "prefilter"
enabled: true
params:
expr: "token == \"super-secret-token\""
- endpoint: /test-hyphen
method: GET
enabled: true
filters:
- ref: "queryParam"
kind: "prefilter"
enabled: true
params:
expr: "foo-bar == \"encrypted-string\""

View File

@@ -0,0 +1,20 @@
apigateway:
- endpoint: /a
method: GET
enabled: true
filters:
- ref: "queryParam"
kind: "prefilter"
enabled: true
params:
expr: "p == \"a\""
- endpoint: /b
method: GET
enabled: true
filters:
- ref: "queryParam"
kind: "prefilter"
enabled: true
params:
expr: "p == \"b\""

View File

@@ -0,0 +1,20 @@
apigateway:
- endpoint: /test
method: GET
enabled: true
filters:
- ref: "queryParam"
kind: "prefilter"
enabled: true
params:
expr: "token == \"super-secret-token\""
- endpoint: /test-hyphen
method: GET
enabled: true
filters:
- ref: "queryParam"
kind: "prefilter"
enabled: true
params:
expr: "params[\"foo-bar\"] == \"encrypted-string\""

View File

@@ -0,0 +1,15 @@
apigateway:
- endpoint: /test
method: GET
enabled: true
filters:
- ref: "payload"
kind: "processer"
enabled: true
params:
jsfunc: |
const x = 10;
const y = 20;
const z = x + y;
return x + y + z

View File

@@ -0,0 +1,15 @@
apigateway:
- endpoint: /test
method: GET
enabled: true
filters:
- ref: "payload"
kind: "processer"
enabled: true
params:
jsfunc: |
const x = 10;
const y = 20;
const z = x + y;
return x + y + z

View File

@@ -0,0 +1,155 @@
package automation
import (
"context"
"errors"
"os"
"testing"
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/automation/rest"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/pkg/api/server"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/label"
ltype "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/rand"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers/sqlite"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/go-chi/chi/v5"
_ "github.com/joho/godotenv/autoload"
"github.com/steinfletcher/apitest"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
)
type (
helper struct {
t *testing.T
a *require.Assertions
cUser *sysTypes.User
roleID uint64
token []byte
}
)
var (
testApp *app.CortezaApp
r chi.Router
eventBus = eventbus.New()
)
func init() {
helpers.RecursiveDotEnvLoad()
}
// random string, 10 chars long by default
func rs(a ...int) string {
var l = 10
if len(a) > 0 {
l = a[0]
}
return string(rand.Bytes(l))
}
func InitTestApp() {
if testApp == nil {
ctx := cli.Context()
testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) {
service.DefaultStore, err = sqlite.ConnectInMemory(ctx)
if err != nil {
return err
}
// Tests should be executed w/o any locales
locale.SetGlobal(locale.Static(&locale.Language{Tag: language.Und}))
eventbus.Set(eventBus)
return nil
})
}
if r == nil {
r = chi.NewRouter()
r.Use(server.BaseMiddleware(false, logger.Default())...)
helpers.BindAuthMiddleware(r)
r.Group(rest.MountRoutes())
}
}
func TestMain(m *testing.M) {
InitTestApp()
os.Exit(m.Run())
}
func newHelper(t *testing.T) helper {
h := helper{
t: t,
a: require.New(t),
roleID: id.Next(),
cUser: &sysTypes.User{
ID: id.Next(),
},
}
h.cUser.SetRoles(h.roleID)
helpers.UpdateRBAC(h.roleID)
var err error
ctx := context.Background()
h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser))
if err != nil {
panic(err)
}
return h
}
func (h helper) MyRole() uint64 {
return h.roleID
}
// Returns context w/ security details
func (h helper) secCtx() context.Context {
return auth.SetIdentityToContext(context.Background(), h.cUser)
}
// apitest basics, initialize, set handler, add auth
func (h helper) apiInit() *apitest.APITest {
InitTestApp()
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderRawAuthBearer(h.token))
}
// Unwraps error before it passes it to the tester
func (h helper) noError(err error) {
for errors.Unwrap(err) != nil {
err = errors.Unwrap(err)
}
h.a.NoError(err)
}
func (h helper) setLabel(res label.LabeledResource, name, value string) {
h.a.NoError(store.UpsertLabel(h.secCtx(), service.DefaultStore, &ltype.Label{
Kind: res.LabelResourceKind(),
ResourceID: res.LabelResourceID(),
Name: name,
Value: ltype.LabelValue{Val: value},
}))
}

View File

@@ -0,0 +1,115 @@
package automation
import (
"fmt"
"net/http"
"testing"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/steinfletcher/apitest-jsonpath"
)
func TestPermissionsEffective(t *testing.T) {
h := newHelper(t)
helpers.DenyMe(h, types.ComponentRbacResource(), "workflow.create")
h.apiInit().
Get("/permissions/effective").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestPermissionsList(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
h.apiInit().
Get("/permissions/").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present(fmt.Sprintf(`$.response[? @.type=="%s"]`, types.ComponentResourceType))).
End()
}
func TestPermissionsRead(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
helpers.DenyMe(h, types.ComponentRbacResource(), "workflow.create")
h.apiInit().
Get(fmt.Sprintf("/permissions/%d/rules", h.roleID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestPermissionsUpdate(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
h.apiInit().
Patch(fmt.Sprintf("/permissions/%d/rules", h.roleID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{"rules":[{"resource":"%s","operation":"workflow.create","access":"allow"}]}`, types.ComponentRbacResource())).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestPermissionsDelete(t *testing.T) {
h := newHelper(t)
p := rbac.Global()
// Make sure our user can grant
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
// New role.
permDelRole := h.roleID + 1
h.a.Len(rbac.Global().FindRulesByRoleID(permDelRole), 0)
// Setup a few fake rules for new role
helpers.Grant(rbac.AllowRule(permDelRole, types.ComponentRbacResource(), "workflow.create"))
h.a.Len(p.FindRulesByRoleID(permDelRole), 1)
h.apiInit().
Delete(fmt.Sprintf("/permissions/%d/rules", permDelRole)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
// Make sure all rules for this role are deleted
for _, r := range p.FindRulesByRoleID(permDelRole) {
h.a.True(r.Access == rbac.Inherit)
}
}
func TestPermissionsTrace(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
h.apiInit().
Get("/permissions/trace").
Query("roleID[]", "1").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present(`$.response`)).
End()
}

View File

@@ -0,0 +1,86 @@
package automation
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
)
func (h helper) clearSessions() {
h.noError(store.TruncateAutomationSessions(context.Background(), service.DefaultStore))
}
func (h helper) repoMakeSession(wf *types.Workflow, ss ...string) *types.Session {
var r = &types.Session{
ID: id.Next(),
CreatedAt: time.Now(),
WorkflowID: wf.ID,
}
if len(ss) > 1 {
r.ResourceType = ss[1]
} else {
r.ResourceType = "h_" + rs()
}
h.a.NoError(store.CreateAutomationSession(context.Background(), service.DefaultStore, r))
return r
}
func TestSessionList(t *testing.T) {
h := newHelper(t)
h.clearSessions()
helpers.AllowMe(h, types.ComponentRbacResource(), "sessions.search")
wf := h.repoMakeWorkflow()
h.repoMakeSession(wf)
h.repoMakeSession(wf)
h.apiInit().
Get("/sessions/").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Len(`$.response.set`, 2)).
End()
}
func TestSessionRead(t *testing.T) {
h := newHelper(t)
h.clearTriggers()
wf := h.repoMakeWorkflow()
s := h.repoMakeSession(wf)
helpers.AllowMe(h, types.ComponentRbacResource(), "sessions.search")
helpers.AllowMe(h, types.WorkflowRbacResource(0), "sessions.manage")
h.apiInit().
Get(fmt.Sprintf("/sessions/%d", s.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestSessionTrace(t *testing.T) {
t.Skip("pending implementation")
}
func TestSessionDelete(t *testing.T) {
t.Skip("pending implementation")
}

View File

@@ -0,0 +1,387 @@
package automation
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/steinfletcher/apitest"
"github.com/steinfletcher/apitest-jsonpath"
"github.com/stretchr/testify/require"
)
func (h helper) clearTriggers() {
h.noError(store.TruncateAutomationTriggers(context.Background(), service.DefaultStore))
}
func (h helper) createTrigger(res *types.Trigger) *types.Trigger {
if res.ID == 0 {
res.ID = id.Next()
}
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
h.a.NoError(service.DefaultStore.CreateAutomationTrigger(context.Background(), res))
return res
}
func (h helper) repoMakeTrigger(wf *types.Workflow, ss ...string) *types.Trigger {
var r = &types.Trigger{
ID: id.Next(),
CreatedAt: time.Now(),
WorkflowID: wf.ID,
}
if len(ss) > 1 {
r.ResourceType = ss[1]
} else {
r.ResourceType = "h_" + rs()
}
r.Enabled = true
h.a.NoError(store.CreateAutomationTrigger(context.Background(), service.DefaultStore, r))
return r
}
func (h helper) lookupTriggerByID(ID uint64) *types.Trigger {
res, err := store.LookupAutomationTriggerByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return res
}
func TestTriggerRead(t *testing.T) {
h := newHelper(t)
h.clearTriggers()
wf := h.repoMakeWorkflow()
tg := h.repoMakeTrigger(wf)
helpers.AllowMe(h, types.ComponentRbacResource(), "triggers.search")
h.apiInit().
Get(fmt.Sprintf("/triggers/%d", tg.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.resourceType`, tg.ResourceType)).
Assert(jsonpath.Equal(`$.response.triggerID`, fmt.Sprintf("%d", tg.ID))).
End()
}
func TestTriggerList(t *testing.T) {
h := newHelper(t)
h.clearTriggers()
helpers.AllowMe(h, types.ComponentRbacResource(), "triggers.search")
wf := h.repoMakeWorkflow()
h.repoMakeTrigger(wf)
h.repoMakeTrigger(wf)
h.apiInit().
Get("/triggers/").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Len(`$.response.set`, 2)).
End()
}
func TestTriggerCreate(t *testing.T) {
h := newHelper(t)
h.clearTriggers()
h.clearWorkflows()
var (
wf = h.repoMakeWorkflow()
req = func() *apitest.Request {
return h.apiInit().
Post("/triggers/").
Header("Accept", "application/json").
FormData("name", rs()).
FormData("workflowID", fmt.Sprintf("%d", wf.ID))
}
)
t.Run("allowed", func(t *testing.T) {
helpers.AllowMe(h, wf.RbacResource(), "triggers.manage")
req().Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
})
t.Run("denied", func(t *testing.T) {
helpers.DenyMe(h, wf.RbacResource(), "triggers.manage")
req().Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("trigger.errors.notAllowedToCreate")).
End()
})
}
func TestTriggerCreateFull(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.WorkflowRbacResource(0), "triggers.manage")
h.clearTriggers()
var (
wf = h.repoMakeWorkflow()
output = &types.Trigger{}
stored = &types.Trigger{}
input = &types.Trigger{
WorkflowID: wf.ID,
ResourceType: "wf-full-test",
Enabled: true,
Input: expr.EmptyVars(),
OwnedBy: 42,
}
)
h.apiInit().
Post("/triggers/").
Header("Accept", "application/json").
JSON(helpers.JSON(input)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&struct{ Response *types.Trigger }{output})
h.a.NotZero(output.ID)
h.a.NotZero(output.OwnedBy)
h.a.NotNil(output.CreatedAt)
h.a.NotZero(output.CreatedBy)
input.ID = output.ID
input.OwnedBy = output.OwnedBy
input.CreatedBy = output.CreatedBy
input.CreatedAt = output.CreatedAt
h.a.NoError(output.Input.ResolveTypes(service.Registry().Type))
input.Meta = output.Meta
h.a.Equal(input, output)
helpers.AllowMe(h, types.ComponentRbacResource(), "triggers.search")
h.apiInit().
Get(fmt.Sprintf("/triggers/%d", output.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&struct{ Response *types.Trigger }{stored})
h.a.NoError(stored.Input.ResolveTypes(service.Registry().Type))
input.Meta = stored.Meta
h.a.Equal(input, stored)
}
func TestTriggerUpdate(t *testing.T) {
h := newHelper(t)
h.clearTriggers()
h.clearWorkflows()
var (
wf = h.repoMakeWorkflow()
tg1 = h.createTrigger(&types.Trigger{WorkflowID: wf.ID, ResourceType: "as-created"})
tg2 = h.createTrigger(&types.Trigger{WorkflowID: wf.ID, ResourceType: "as-created"})
req = func(id uint64, resType string) *apitest.Request {
return h.apiInit().
Put(fmt.Sprintf("/triggers/%d", id)).
Header("Accept", "application/json").
FormData("resourceType", resType)
}
)
t.Run("allowed", func(t *testing.T) {
helpers.AllowMe(h, wf.RbacResource(), "triggers.manage")
req(tg1.ID, "foo").Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res := h.lookupTriggerByID(tg1.ID)
h.a.NotNil(res)
h.a.Equal("foo", res.ResourceType)
})
t.Run("denied", func(t *testing.T) {
helpers.DenyMe(h, wf.RbacResource(), "triggers.manage")
req(tg2.ID, "bar").Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("trigger.errors.notAllowedToUpdate")).
End()
res := h.lookupTriggerByID(tg2.ID)
h.a.NotNil(res)
h.a.NotEqual("bar", res.ResourceType)
})
}
func TestTriggerDeleteForbidden(t *testing.T) {
h := newHelper(t)
wf := h.repoMakeWorkflow()
res := h.repoMakeTrigger(wf)
h.apiInit().
Delete(fmt.Sprintf("/triggers/%d", res.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("trigger.errors.notAllowedToDelete")).
End()
res = h.lookupTriggerByID(res.ID)
h.a.NotNil(res)
h.a.Nil(res.DeletedAt)
}
func TestTriggerDelete(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.WorkflowRbacResource(0), "triggers.manage")
wf := h.repoMakeWorkflow()
res := h.repoMakeTrigger(wf)
h.apiInit().
Delete(fmt.Sprintf("/triggers/%d", res.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupTriggerByID(res.ID)
h.a.NotNil(res)
h.a.NotNil(res.DeletedAt)
}
func TestTriggerUndelete(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.WorkflowRbacResource(0), "triggers.manage")
wf := h.repoMakeWorkflow()
res := h.repoMakeTrigger(wf)
h.apiInit().
Post(fmt.Sprintf("/triggers/%d/undelete", res.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupTriggerByID(res.ID)
h.a.NotNil(res)
h.a.Nil(res.DeletedAt)
}
func TestTriggerLabels(t *testing.T) {
h := newHelper(t)
h.clearTriggers()
helpers.AllowMe(h, types.WorkflowRbacResource(0), "triggers.manage")
helpers.AllowMe(h, types.ComponentRbacResource(), "triggers.search")
var (
ID uint64
wf = h.repoMakeWorkflow()
)
t.Run("create", func(t *testing.T) {
var (
req = require.New(t)
payload = &types.Trigger{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
"/triggers/",
types.Trigger{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "bar"}, "bar": {Val: "42"}}, WorkflowID: wf.ID},
payload,
)
req.NotZero(payload.ID)
h.a.Equal(payload.Labels["foo"].Val, "bar",
"labels must contain foo with value bar")
h.a.Equal(payload.Labels["bar"].Val, "42",
"labels must contain bar with value 42")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
ID = payload.ID
})
t.Run("update", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
payload = &types.Trigger{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("PUT /triggers/%d", ID),
&types.Trigger{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "baz"}, "baz": {Val: "123"}}, WorkflowID: wf.ID},
payload,
)
req.NotZero(payload.ID)
//req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels")
req.Equal(payload.Labels["foo"].Val, "baz",
"labels must contain foo with value baz")
req.NotContains(payload.Labels, "bar",
"labels must not contain bar")
req.Equal(payload.Labels["baz"].Val, "123",
"labels must contain baz with value 123")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
})
t.Run("search", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
set = types.TriggerSet{}
params = url.Values{}
)
params.Add("labels", "baz=123")
params.Add("disabled", filter.StateInclusive.String())
helpers.SearchWithLabelsViaAPI(h.apiInit(), t, "/triggers/", &set, params)
req.NotEmpty(set)
req.NotNil(set.FindByID(ID))
req.NotNil(set.FindByID(ID).Labels)
})
}

View File

@@ -0,0 +1,504 @@
package automation
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/cortezaproject/corteza/server/automation/service"
"github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/steinfletcher/apitest-jsonpath"
"github.com/stretchr/testify/require"
)
func (h helper) clearWorkflows() {
h.noError(store.TruncateAutomationWorkflows(context.Background(), service.DefaultStore))
}
func (h helper) createWorkflow(res *types.Workflow) *types.Workflow {
if res.ID == 0 {
res.ID = id.Next()
}
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
h.a.NoError(service.DefaultStore.CreateAutomationWorkflow(context.Background(), res))
return res
}
func (h helper) repoMakeWorkflow(ss ...string) *types.Workflow {
var r = &types.Workflow{
ID: id.Next(),
CreatedAt: time.Now(),
Enabled: true,
}
if len(ss) > 1 {
r.Handle = ss[1]
} else {
r.Handle = "h_" + rs()
}
h.a.NoError(store.CreateAutomationWorkflow(context.Background(), service.DefaultStore, r))
return r
}
func (h helper) lookupWorkflowByID(ID uint64) *types.Workflow {
res, err := store.LookupAutomationWorkflowByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return res
}
func TestWorkflowRead(t *testing.T) {
h := newHelper(t)
h.clearWorkflows()
wf := h.repoMakeWorkflow()
helpers.AllowMe(h, wf.RbacResource(), "read")
h.apiInit().
Get(fmt.Sprintf("/workflows/%d", wf.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.handle`, wf.Handle)).
Assert(jsonpath.Equal(`$.response.workflowID`, fmt.Sprintf("%d", wf.ID))).
End()
}
func TestWorkflowList(t *testing.T) {
h := newHelper(t)
h.clearWorkflows()
helpers.AllowMe(h, types.ComponentRbacResource(), "workflows.search")
helpers.AllowMe(h, types.WorkflowRbacResource(0), "read")
h.repoMakeWorkflow()
h.repoMakeWorkflow()
h.apiInit().
Get("/workflows/").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Len(`$.response.set`, 2)).
End()
}
func TestWorkflowList_filterForbidden(t *testing.T) {
h := newHelper(t)
h.repoMakeWorkflow("workflow")
f := h.repoMakeWorkflow()
helpers.AllowMe(h, types.ComponentRbacResource(), "workflows.search")
helpers.DenyMe(h, f.RbacResource(), "read")
h.apiInit().
Get("/workflows/").
Query("handle", f.Handle).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.NotPresent(fmt.Sprintf(`$.response.set[? @.handle=="%s"]`, f.Handle))).
End()
}
func TestWorkflowCreateForbidden(t *testing.T) {
h := newHelper(t)
h.apiInit().
Post("/workflows/").
Header("Accept", "application/json").
FormData("name", rs()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("workflow.errors.notAllowedToCreate")).
End()
}
func TestWorkflowCreateNotUnique(t *testing.T) {
h := newHelper(t)
h.clearWorkflows()
helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create")
workflow := h.repoMakeWorkflow()
h.apiInit().
Post("/workflows/").
Header("Accept", "application/json").
FormData("meta", fmt.Sprintf(`{"name": "%s"}`, rs())).
FormData("handle", workflow.Handle).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("workflow.errors.handleNotUnique")).
End()
}
func TestWorkflowCreate(t *testing.T) {
h := newHelper(t)
h.clearWorkflows()
helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create")
h.apiInit().
Post("/workflows/").
FormData("meta", fmt.Sprintf(`{"name": "%s"}`, rs())).
FormData("handle", "handle_"+rs()).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestWorkflowCreateFull(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create")
h.clearWorkflows()
var (
output = &types.Workflow{}
stored = &types.Workflow{}
input = &types.Workflow{
Handle: "wf-full-test",
Meta: &types.WorkflowMeta{
Name: "name",
Description: "desc",
Visual: map[string]interface{}{"foo": "bar"},
},
Enabled: true,
Trace: true,
KeepSessions: 10000,
Scope: func() *expr.Vars {
o := &expr.Vars{}
_ = o.Set("foo", "bar")
return o
}(),
Steps: types.WorkflowStepSet{},
Paths: types.WorkflowPathSet{},
RunAs: 0,
OwnedBy: 42,
}
)
h.apiInit().
Post("/workflows/").
Header("Accept", "application/json").
JSON(helpers.JSON(input)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&struct{ Response *types.Workflow }{output})
h.a.NotZero(output.ID)
h.a.NotZero(output.OwnedBy)
h.a.NotNil(output.CreatedAt)
h.a.NotZero(output.CreatedBy)
// Make comparing input&output a bit easier
input.ID = output.ID
input.OwnedBy = output.OwnedBy
input.CreatedBy = output.CreatedBy
input.CreatedAt = output.CreatedAt
h.a.NoError(output.Scope.ResolveTypes(service.Registry().Type))
h.a.Equal(input, output)
helpers.AllowMe(h, output.RbacResource(), "read")
h.apiInit().
Get(fmt.Sprintf("/workflows/%d", output.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&struct{ Response *types.Workflow }{stored})
h.a.NoError(stored.Scope.ResolveTypes(service.Registry().Type))
h.a.Equal(input, stored)
}
func TestWorkflowUpdateForbidden(t *testing.T) {
h := newHelper(t)
h.clearWorkflows()
u := h.repoMakeWorkflow()
h.apiInit().
Put(fmt.Sprintf("/workflows/%d", u.ID)).
Header("Accept", "application/json").
FormData("meta", fmt.Sprintf(`{"name": "%s"}`, rs())).
FormData("email", rs()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("workflow.errors.notAllowedToUpdate")).
End()
}
func TestWorkflowUpdate(t *testing.T) {
h := newHelper(t)
h.clearWorkflows()
res := h.repoMakeWorkflow()
helpers.AllowMe(h, types.WorkflowRbacResource(0), "update")
newName := "updated-" + rs()
newHandle := "updated-" + rs()
h.apiInit().
Put(fmt.Sprintf("/workflows/%d", res.ID)).
FormData("meta", fmt.Sprintf(`{"name": "%s"}`, newName)).
FormData("handle", newHandle).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupWorkflowByID(res.ID)
h.a.NotNil(res)
h.a.Equal(newHandle, res.Handle)
}
func TestWorkflowDeleteForbidden(t *testing.T) {
h := newHelper(t)
u := h.repoMakeWorkflow()
h.apiInit().
Delete(fmt.Sprintf("/workflows/%d", u.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("workflow.errors.notAllowedToDelete")).
End()
}
func TestWorkflowDelete(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.WorkflowRbacResource(0), "delete")
res := h.repoMakeWorkflow()
h.apiInit().
Delete(fmt.Sprintf("/workflows/%d", res.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupWorkflowByID(res.ID)
h.a.NotNil(res)
h.a.NotNil(res.DeletedAt)
}
func TestWorkflowUndelete(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.WorkflowRbacResource(0), "undelete")
res := h.repoMakeWorkflow()
h.apiInit().
Post(fmt.Sprintf("/workflows/%d/undelete", res.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupWorkflowByID(res.ID)
h.a.NotNil(res)
h.a.Nil(res.DeletedAt)
}
func TestWorkflowLabels(t *testing.T) {
h := newHelper(t)
h.clearWorkflows()
helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create", "workflows.search")
helpers.AllowMe(h, types.WorkflowRbacResource(0), "read", "update", "delete")
var (
ID uint64
)
t.Run("create", func(t *testing.T) {
var (
req = require.New(t)
payload = &types.Workflow{Meta: &types.WorkflowMeta{Name: rs()}}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
"/workflows/",
types.Workflow{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "bar"}, "bar": {Val: "42"}}, Meta: &types.WorkflowMeta{Name: rs()}},
payload,
)
req.NotZero(payload.ID)
h.a.Equal(payload.Labels["foo"].Val, "bar",
"labels must contain foo with value bar")
h.a.Equal(payload.Labels["bar"].Val, "42",
"labels must contain bar with value 42")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
ID = payload.ID
})
t.Run("update", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
payload = &types.Workflow{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("PUT /workflows/%d", ID),
&types.Workflow{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "baz"}, "baz": {Val: "123"}}, Meta: &types.WorkflowMeta{Name: rs()}},
payload,
)
req.NotZero(payload.ID)
// req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels")
req.Equal(payload.Labels["foo"].Val, "baz",
"labels must contain foo with value baz")
req.NotContains(payload.Labels, "bar",
"labels must not contain bar")
req.Equal(payload.Labels["baz"].Val, "123",
"labels must contain baz with value 123")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
})
t.Run("search", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
set = types.WorkflowSet{}
params = url.Values{}
)
params.Add("labels", "baz=123")
params.Add("disabled", filter.StateInclusive.String())
helpers.SearchWithLabelsViaAPI(h.apiInit(), t, "/workflows/", &set, params)
req.NotEmpty(set)
req.NotNil(set.FindByID(ID))
req.NotNil(set.FindByID(ID).Labels)
})
}
func TestWorkflowStepsPayload(t *testing.T) {
wf := &types.Workflow{}
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "workflow.create")
h.apiInit().
Post("/workflows/").
JSON(`{
"scope": {
"foo": { "@type": "String", "@value": "" },
"bar": { "@type": "String", "@value": "" },
"baz": { "@type": "String", "@value": "" },
"hgtg": { "@type": "Integer", "@value": 42 }
},
"meta": {
"name": "name of the workdflow",
"description": "lorem ipsum dolor sit...",
"visual": { "arbitrary key": "arbitrary value" }
},
"steps": [
{ "stepID": "100",
"kind": "expressions",
"arguments": [
{ "name": "foo", "expr": "\"foo\"" },
{ "name": "bar", "expr": "\"bar\"" }
],
"meta": { "name": "step name text", "description": "step description text", "visual": { "arbitrary key": "arbitrary value" }}
},
{ "stepID": "101",
"kind": "expressions",
"arguments": [
{ "name": "foo", "expr": "\"foo\"" },
{ "name": "bar", "expr": "\"bar\"" }
]
},
{ "stepID": "200", "kind": "gateway", "ref": "join" },
{ "stepID": "201", "kind": "gateway", "ref": "fork" },
{ "stepID": "202", "kind": "gateway", "ref": "excl" },
{ "stepID": "203", "kind": "gateway", "ref": "incl" },
{ "stepID": "300",
"kind": "function",
"ref": "serverSideFuncRef",
"meta": { "description": "ref needs to come from a predefined list of registered functions; arguments pick values from scope and need to be compatible with registered function's parameters; same goes for results" },
"arguments": [
{ "name": "arg1", "expr": "foo" },
{ "name": "arg2", "expr": "hgtg * 2" }
],
"results": [
{ "name": "scopeVar1", "expr": "nameOfVarFromFunctionsResult1" },
{ "name": "scopeVar2", "expr": "nameOfVarFromFunctionsResult2" }
]
},
{ "stepID": "400",
"kind": "prompt",
"ref": "clientPromptRef",
"meta": { "description": "backed does not need to know about ref, it only cares that input data (when resuming session) is compatible with arguments (and tests there)" },
"arguments": [
{ "name": "foo", "expr": "inputVar1",
"tests": [
{ "expr": "inputVar1 != \"\"", "error": "expecting inputVar1 to be non-empty string" }
]
},
{ "name": "bar", "expr": "inputVar2" }
]
}
],
"paths": [
{ "parentID": "201", "childID": "300", "expr": "hgtg == 42",
"meta": { "name": "...", "description": "only if equals 42" } },
{ "parentID": "201", "childID": "400",
"meta": { "name": "...", "description": "else" } }
]
}`).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&struct{ Response *types.Workflow }{Response: wf})
}

View File

@@ -0,0 +1,81 @@
package compose
import (
"context"
"fmt"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
"net/http"
"testing"
"time"
)
func (h helper) clearAttachment() {
h.clearNamespaces()
h.noError(store.TruncateAttachments(context.Background(), service.DefaultStore))
}
func (h helper) repoMakeAttachment(ss ...string) *types.Attachment {
var res = &types.Attachment{
ID: id.Next(),
CreatedAt: time.Now(),
Kind: types.RecordAttachment,
}
if len(ss) > 0 {
res.Name = ss[0]
} else {
res.Name = "n_" + rs()
}
h.a.NoError(store.CreateComposeAttachment(context.Background(), service.DefaultStore, res))
return res
}
func (h helper) lookupAttachmentByID(ID uint64) *types.Attachment {
res, err := store.LookupComposeAttachmentByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return res
}
func TestAttachmentRead(t *testing.T) {
h := newHelper(t)
h.clearAttachment()
ns := h.makeNamespace("some-namespace")
a := h.repoMakeAttachment()
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/attachment/%s/%d", ns.ID, a.Kind, a.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.name`, a.Name)).
Assert(jsonpath.Equal(`$.response.attachmentID`, fmt.Sprintf("%d", a.ID))).
End()
}
func TestAttachmentDelete(t *testing.T) {
h := newHelper(t)
h.clearAttachment()
ns := h.makeNamespace("some-namespace")
a := h.repoMakeAttachment()
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/attachment/%s/%d", ns.ID, a.Kind, a.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
a = h.lookupAttachmentByID(a.ID)
h.a.NotNil(a)
h.a.NotNil(a.DeletedAt)
}

View File

@@ -0,0 +1,318 @@
package compose
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/id"
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
"github.com/stretchr/testify/require"
)
func (h helper) clearCharts() {
h.clearNamespaces()
h.noError(store.TruncateComposeCharts(context.Background(), service.DefaultStore))
}
func (h helper) makeChart(ns *types.Namespace, name string) *types.Chart {
res := &types.Chart{
ID: id.Next(),
CreatedAt: time.Now(),
Name: name,
Handle: name,
NamespaceID: ns.ID,
}
h.noError(store.CreateComposeChart(context.Background(), service.DefaultStore, res))
return res
}
func (h helper) lookupChartByID(ID uint64) *types.Chart {
res, err := store.LookupComposeChartByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return res
}
func TestChartRead(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
m := h.makeChart(ns, "some-chart")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/chart/%d", ns.ID, m.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.name`, m.Name)).
Assert(jsonpath.Equal(`$.response.chartID`, fmt.Sprintf("%d", m.ID))).
End()
}
func TestChartReadByHandle(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
c := h.makeChart(ns, "some-chart")
cbh, err := service.DefaultChart.FindByHandle(h.secCtx(), ns.ID, c.Handle)
h.noError(err)
h.a.NotNil(cbh)
h.a.Equal(cbh.ID, c.ID)
h.a.Equal(cbh.Handle, c.Handle)
}
func TestChartList(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
ns := h.makeNamespace("some-namespace")
h.makeChart(ns, "chart1")
h.makeChart(ns, "chart2")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/chart/", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestChartList_filterForbidden(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
ns := h.makeNamespace("some-namespace")
h.makeChart(ns, "chart")
f := h.makeChart(ns, "chart_forbidden")
helpers.DenyMe(h, f.RbacResource(), "read")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/chart/", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.NotPresent(`$.response.set[? @.name=="chart_forbidden"]`)).
End()
}
func TestChartCreateForbidden(t *testing.T) {
h := newHelper(t)
h.clearCharts()
ns := h.makeNamespace("some-namespace")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/chart/", ns.ID)).
Header("Accept", "application/json").
FormData("name", "some-chart").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("chart.errors.notAllowedToCreate")).
End()
}
func TestChartCreate(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "chart.create")
ns := h.makeNamespace("some-namespace")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/chart/", ns.ID)).
FormData("name", "some-chart").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestChartUpdateForbidden(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
ns := h.makeNamespace("some-namespace")
m := h.makeChart(ns, "some-chart")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/chart/%d", ns.ID, m.ID)).
Header("Accept", "application/json").
FormData("name", "changed-name").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("chart.errors.notAllowedToUpdate")).
End()
}
func TestChartUpdate(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
ns := h.makeNamespace("some-namespace")
res := h.makeChart(ns, "some-chart")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "update")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/chart/%d", ns.ID, res.ID)).
FormData("name", "changed-name").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupChartByID(res.ID)
h.a.NotNil(res)
h.a.Equal(res.Name, "changed-name")
}
func TestChartDeleteForbidden(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
m := h.makeChart(ns, "some-chart")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/chart/%d", ns.ID, m.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("chart.errors.notAllowedToDelete")).
End()
}
func TestChartDelete(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "delete")
ns := h.makeNamespace("some-namespace")
res := h.makeChart(ns, "some-chart")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/chart/%d", ns.ID, res.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupChartByID(res.ID)
h.a.NotNil(res.DeletedAt)
}
func TestChartLabels(t *testing.T) {
h := newHelper(t)
h.clearCharts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "charts.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "chart.create")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "update")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "delete")
var (
ns = h.makeNamespace("some-namespace")
ID uint64
)
t.Run("create", func(t *testing.T) {
var (
req = require.New(t)
payload = &types.Chart{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d/chart/", ns.ID),
types.Chart{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "bar"}, "bar": {Val: "42"}}},
payload,
)
req.NotZero(payload.ID)
h.a.Equal(payload.Labels["foo"].Val, "bar",
"labels must contain foo with value bar")
h.a.Equal(payload.Labels["bar"].Val, "42",
"labels must contain bar with value 42")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
ID = payload.ID
})
t.Run("update", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
payload = &types.Chart{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d/chart/%d", ns.ID, ID),
types.Chart{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "baz"}, "baz": {Val: "123"}}},
payload,
)
req.NotZero(payload.ID)
req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels")
req.Equal(payload.Labels["foo"].Val, "baz",
"labels must contain foo with value baz")
req.NotContains(payload.Labels, "bar",
"labels must not contain bar")
req.Equal(payload.Labels["baz"].Val, "123",
"labels must contain baz with value 123")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
})
t.Run("search", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
set = types.ChartSet{}
)
helpers.SearchWithLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d/chart/", ns.ID),
&set, url.Values{"labels": []string{"baz=123"}},
)
req.NotEmpty(set)
req.NotNil(set.FindByID(ID))
req.NotNil(set.FindByID(ID).Labels)
})
}

View File

@@ -0,0 +1,440 @@
package compose
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path"
"strings"
"testing"
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/compose/rest"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api/server"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoy"
"github.com/cortezaproject/corteza/server/pkg/envoy/csv"
"github.com/cortezaproject/corteza/server/pkg/envoy/directory"
"github.com/cortezaproject/corteza/server/pkg/envoy/resource"
envoyStore "github.com/cortezaproject/corteza/server/pkg/envoy/store"
"github.com/cortezaproject/corteza/server/pkg/envoy/yaml"
"github.com/cortezaproject/corteza/server/pkg/eventbus"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/objstore/plain"
"github.com/cortezaproject/corteza/server/pkg/rand"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/go-chi/chi/v5"
_ "github.com/joho/godotenv/autoload"
"github.com/spf13/afero"
"github.com/steinfletcher/apitest"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
)
type (
helper struct {
t *testing.T
a *require.Assertions
cUser *sysTypes.User
roleID uint64
token []byte
}
dalSvc interface {
dal.FullService
Purge(ctx context.Context)
}
)
var (
testApp *app.CortezaApp
r chi.Router
testUser *sysTypes.User
eventBus = eventbus.New()
defStore store.Storer
defDal dalSvc
)
func init() {
helpers.RecursiveDotEnvLoad()
}
// random string, 10 chars long by default
func rs(a ...int) string {
var l = 10
if len(a) > 0 {
l = a[0]
}
return string(rand.Bytes(l))
}
func InitTestApp() {
if testApp == nil {
ctx := cli.Context()
testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) {
service.DefaultStore = app.Store
defStore = app.Store
service.DefaultObjectStore, err = plain.NewWithAfero(afero.NewMemMapFs(), "test")
if err != nil {
return err
}
// Tests should be executed w/o any locales
locale.SetGlobal(locale.Static(&locale.Language{Tag: language.Und}))
eventbus.Set(eventBus)
return nil
})
defDal = dal.Service()
}
if r == nil {
r = chi.NewRouter()
r.Use(server.BaseMiddleware(false, logger.Default())...)
helpers.BindAuthMiddleware(r)
r.Group(rest.MountRoutes())
}
}
func TestMain(m *testing.M) {
InitTestApp()
os.Exit(m.Run())
}
func newHelper(t *testing.T) helper {
ctx := context.Background()
h := helper{
t: t,
a: require.New(t),
roleID: id.Next(),
}
if testUser == nil {
testUser = &sysTypes.User{
Handle: fmt.Sprintf("test_user_%d", id.Next()),
Name: "test_user",
ID: id.Next(),
}
err := store.CreateUser(ctx, service.DefaultStore, testUser)
if err != nil {
panic(err)
}
}
h.cUser = testUser
h.cUser.SetRoles(h.roleID)
helpers.UpdateRBAC(h.roleID)
h.identityToHelper(h.cUser)
return h
}
func (h *helper) identityToHelper(u *sysTypes.User) {
var err error
h.cUser = u
ctx := context.Background()
h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser))
if err != nil {
panic(err)
}
}
func (h helper) MyRole() uint64 {
return h.roleID
}
// Returns context w/ security details
func (h helper) secCtx() context.Context {
return auth.SetIdentityToContext(context.Background(), h.cUser)
}
// apitest basics, initialize, set handler, add auth
func (h helper) apiInit() *apitest.APITest {
InitTestApp()
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderRawAuthBearer(h.token))
}
func (h helper) mockPermissions(rules ...*rbac.Rule) {
h.noError(rbac.Global().Grant(
// TestService we use does not have any backend storage,
context.Background(),
rules...,
))
}
// Unwraps error before it passes it to the tester
func (h helper) noError(err error) {
for errors.Unwrap(err) != nil {
err = errors.Unwrap(err)
}
h.a.NoError(err)
}
func collect(ee ...error) error {
for _, e := range ee {
if e != nil {
return e
}
}
return nil
}
func cleanup(t *testing.T) {
var (
ctx = context.Background()
)
err := collect(
store.TruncateComposeNamespaces(ctx, defStore),
store.TruncateComposePages(ctx, defStore),
store.TruncateComposeModuleFields(ctx, defStore),
store.TruncateComposeModules(ctx, defStore),
)
if err != nil {
t.Fatalf("failed to decode scenario data: %v", err)
}
err = truncateRecords(ctx)
if err != nil {
t.Fatalf("failed to truncate records: %v", err)
}
defDal.Purge(ctx)
}
func truncateRecords(ctx context.Context) error {
models, err := defDal.SearchModels(ctx)
if err != nil {
return err
}
for _, model := range models {
err = defDal.Truncate(ctx, model.ToFilter(), nil)
if err != nil {
return err
}
}
return nil
}
func scenario(t *testing.T) string {
return t.Name()[5:]
}
func loadScenario(ctx context.Context, s store.Storer, t *testing.T, h helper) {
loadScenarioWithName(ctx, s, t, h, scenario(t))
}
func loadScenarioWithName(ctx context.Context, s store.Storer, t *testing.T, h helper, scenario string) {
cleanup(t)
parseEnvoy(ctx, s, h, path.Join("testdata", scenario, "data_model"))
}
func parseEnvoy(ctx context.Context, s store.Storer, h helper, path string) {
nn, err := directory.Decode(
ctx,
path,
yaml.Decoder(),
csv.Decoder(),
)
if err != nil {
h.t.Fatalf("failed to decode scenario data: %v", err)
}
crs := resource.ComposeRecordShaper()
nn, err = resource.Shape(nn, crs)
h.a.NoError(err)
// import into the store
se := envoyStore.NewStoreEncoder(s, dal.Service(), &envoyStore.EncoderConfig{})
bld := envoy.NewBuilder(se)
g, err := bld.Build(ctx, nn...)
h.a.NoError(err)
err = envoy.Encode(ctx, g, se)
h.a.NoError(err)
}
func bypassRBAC(ctx context.Context) context.Context {
u := &sysTypes.User{
ID: id.Next(),
}
u.SetRoles(auth.BypassRoles().IDs()...)
return auth.SetIdentityToContext(ctx, u)
}
func setup(t *testing.T) (context.Context, helper, store.Storer) {
h := newHelper(t)
s := service.DefaultStore
u := &sysTypes.User{
ID: id.Next(),
}
u.SetRoles(auth.BypassRoles().IDs()...)
ctx := auth.SetIdentityToContext(context.Background(), u)
return ctx, h, s
}
func grantImportExport(h helper) {
helpers.AllowMe(h, types.ComponentRbacResource(), "namespace.create")
helpers.AllowMe(h, types.ComponentRbacResource(), "module.create")
helpers.AllowMe(h, types.ComponentRbacResource(), "page.create")
helpers.AllowMe(h, types.ComponentRbacResource(), "chart.create")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "modules.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "charts.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "pages.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read")
helpers.AllowMe(h, sysTypes.ComponentRbacResource(), "roles.search")
helpers.AllowMe(h, sysTypes.RoleRbacResource(0), "read")
}
func namespaceExportSafe(t *testing.T, h helper, namespaceID uint64) []byte {
bb, err := namespaceExport(t, h, namespaceID)
h.a.NoError(err)
return bb
}
func namespaceExport(t *testing.T, h helper, namespaceID uint64) ([]byte, error) {
out := h.apiInit().
Get(fmt.Sprintf("/namespace/%d/export/out.zip?jwt=%s", namespaceID, h.token)).
Expect(t).
Status(http.StatusOK).
End()
defer out.Response.Body.Close()
bb, err := ioutil.ReadAll(out.Response.Body)
if err != nil {
return nil, err
}
if strings.HasPrefix(string(bb), "Error:") {
return nil, errors.New(string(bb))
}
return bb, nil
}
func namespaceImportInitPathSafe(t *testing.T, h helper, pp ...string) uint64 {
sessionID, err := namespaceImportInitPath(t, h, pp...)
h.a.NoError(err)
return sessionID
}
func namespaceImportInitPath(t *testing.T, h helper, pp ...string) (uint64, error) {
f, err := os.Open(testSource(t, pp...))
if err != nil {
return 0, err
}
defer f.Close()
bb, err := ioutil.ReadAll(f)
if err != nil {
return 0, err
}
return namespaceImportInit(t, h, bb)
}
func namespaceImportInitSafe(t *testing.T, h helper, arch []byte) uint64 {
sessionID, err := namespaceImportInit(t, h, arch)
h.a.NoError(err)
return sessionID
}
func namespaceImportInit(t *testing.T, h helper, arch []byte) (uint64, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("upload", "archive.zip")
h.noError(err)
_, err = part.Write(arch)
h.noError(err)
h.noError(writer.Close())
out := h.apiInit().
Post("/namespace/import").
Header("Accept", "application/json").
Body(body.String()).
ContentType(writer.FormDataContentType()).
Expect(h.t).
Status(http.StatusOK).
End()
defer out.Response.Body.Close()
bb, err := ioutil.ReadAll(out.Response.Body)
h.a.NoError(err)
var aux struct {
Error struct {
Message string
}
Response struct {
ID uint64 `json:"sessionID,string"`
}
}
h.a.NoError(json.Unmarshal(bb, &aux))
if aux.Error.Message != "" {
return 0, errors.New(aux.Error.Message)
}
return aux.Response.ID, nil
}
func namespaceImportRun(ctx context.Context, s store.Storer, t *testing.T, h helper, sessionID uint64, name, slug string) (*types.Namespace, types.ModuleSet, types.PageSet, types.ChartSet) {
h.apiInit().
Post(fmt.Sprintf("/namespace/import/%d", sessionID)).
Header("Accept", "application/json").
FormData("name", name).
FormData("slug", slug).
Expect(h.t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ns, mm, pp, cc, _, err := fetchEntireNamespace(ctx, s, slug)
h.a.NoError(err)
return ns, mm, pp, cc
}
func testSource(t *testing.T, pp ...string) string {
return path.Join(append([]string{"testdata", scenario(t)}, pp...)...)
}

View File

@@ -0,0 +1,954 @@
package compose
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/id"
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
"github.com/stretchr/testify/require"
)
func (h helper) clearModules() {
h.clearNamespaces()
h.noError(store.TruncateComposeModules(context.Background(), service.DefaultStore))
h.noError(store.TruncateComposeModuleFields(context.Background(), service.DefaultStore))
models, err := defDal.SearchModels(context.Background())
h.noError(err)
for _, m := range models {
h.noError(defDal.RemoveModel(context.Background(), m.ConnectionID, m.ResourceID))
}
}
func (h helper) makeModule(ns *types.Namespace, name string, ff ...*types.ModuleField) *types.Module {
return h.createModule(ns, &types.Module{
Name: name,
NamespaceID: ns.ID,
Fields: ff,
CreatedAt: time.Now(),
})
}
func (h helper) createModule(ns *types.Namespace, res *types.Module) *types.Module {
res.ID = id.Next()
res.CreatedAt = time.Now()
h.noError(store.CreateComposeModule(context.Background(), service.DefaultStore, res))
if res.Config.DAL.ConnectionID == 0 {
res.Config.DAL.ConnectionID = defDal.GetConnectionByID(0).ID
}
_ = res.Fields.Walk(func(f *types.ModuleField) error {
f.ID = id.Next()
f.ModuleID = res.ID
f.NamespaceID = res.NamespaceID
f.CreatedAt = time.Now()
return nil
})
h.noError(store.CreateComposeModuleField(context.Background(), service.DefaultStore, res.Fields...))
h.noError(service.DalModelReplace(context.Background(), service.DefaultStore, nil, defDal, ns, res))
return res
}
func (h helper) lookupModuleByID(ID uint64) *types.Module {
res, err := store.LookupComposeModuleByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
res.Fields, _, err = store.SearchComposeModuleFields(context.Background(), service.DefaultStore, types.ModuleFieldFilter{ModuleID: []uint64{ID}})
h.noError(err)
return res
}
func TestModuleRead(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.name`, m.Name)).
Assert(jsonpath.Equal(`$.response.moduleID`, fmt.Sprintf("%d", m.ID))).
End()
}
func TestModuleReadByHandle(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
c := h.makeModule(ns, "some-module")
cbh, err := service.DefaultModule.FindByHandle(h.secCtx(), ns.ID, c.Handle)
h.noError(err)
h.a.NotNil(cbh)
h.a.Equal(cbh.ID, c.ID)
h.a.Equal(cbh.Handle, c.Handle)
}
func TestModuleList(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
h.makeModule(ns, "app")
h.makeModule(ns, "app")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/module/", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestModuleListQuery(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
h.createModule(ns, &types.Module{
Name: "name",
Handle: "handle",
NamespaceID: ns.ID,
})
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/module/", ns.ID)).
Query("query", "handle").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.set != null")).
End()
}
func TestModuleList_filterForbidden(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
h.makeModule(ns, "module")
f := h.makeModule(ns, "module_forbidden")
helpers.DenyMe(h, types.ModuleRbacResource(0, f.ID), "read")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/module/", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.NotPresent(`$.response.set[? @.name=="module_forbidden"]`)).
End()
}
func TestModuleCreateForbidden(t *testing.T) {
h := newHelper(t)
h.clearModules()
ns := h.makeNamespace("some-namespace")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/", ns.ID)).
Header("Accept", "application/json").
FormData("name", "some-module").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("module.errors.notAllowedToCreate")).
End()
}
func TestModuleCreate(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "module.create")
ns := h.makeNamespace("some-namespace")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/", ns.ID)).
FormData("name", "some-module").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestModuleCreateInvalidField(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "module.create")
ns := h.makeNamespace("some-namespace")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/", ns.ID)).
JSON(`{ "name": "mod", "fields": [{ "name": "a", "kind": "Number" }] }`).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("module.errors.invalidHandle")).
End()
}
func TestModuleUpdateForbidden(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
Header("Accept", "application/json").
FormData("name", "changed-name").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("module.errors.notAllowedToUpdate")).
End()
}
func TestModuleUpdate(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
FormData("name", "changed-name").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.updatedAt")).
End()
m, err := store.LookupComposeModuleByID(context.Background(), service.DefaultStore, m.ID)
h.noError(err)
h.a.NotNil(m)
h.a.Equal("changed-name", m.Name)
}
func TestModuleFieldsUpdate(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "existing"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "existing_edited", "kind": "Number" }, { "name": "new", "kind": "DateTime" }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 2)
h.a.NotNil(m.Fields[0].UpdatedAt)
h.a.Equal(m.Fields[0].Name, "existing_edited")
h.a.Equal(m.Fields[0].Kind, "Number")
h.a.Nil(m.Fields[1].UpdatedAt)
h.a.Equal(m.Fields[1].Name, "new")
h.a.Equal(m.Fields[1].Kind, "DateTime")
}
func TestModuleFieldsUpdate_invalidHandle(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "module.create")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
ns := h.makeNamespace("some-namespace")
mod := h.makeModule(ns, "mod", &types.ModuleField{
Name: "a",
Kind: "String",
})
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, mod.ID)).
JSON(`{ "name": "mod", "fields": [{ "name": "a", "kind": "String" }] }`).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestModuleUpdateWithReservedFieldName(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "ownedBy"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "ownedBy", "kind": "Number" }]}`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
Header("Accept", "application/json").
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
f = m.Fields[0]
fjs = fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "updatedBy", "kind": "Number" }]}`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
Header("Accept", "application/json").
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("module.errors.fieldNameReserved")).
End()
}
func TestModuleCreateWithReservedFieldName(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
fjs := fmt.Sprintf(`{ "name": "foo", "fields": [{ "name": "ownedBy", "kind": "Number" }]}`)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/", ns.ID)).
Header("Accept", "application/json").
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("module.errors.fieldNameReserved")).
End()
}
func TestModuleFieldsUpdate_defaults(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "existing", Required: true, DefaultValue: types.RecordValueSet{&types.RecordValue{Value: "test"}}})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "existing_edited", "kind": "String", "isRequired": true, "defaultValue": [{ "value": "test" }] }, { "name": "new", "kind": "Bool", "defaultValue": [{ "value": "1" }] }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 2)
h.a.Equal(m.Fields[0].Kind, "String")
h.a.Len(m.Fields[0].DefaultValue, 1)
h.a.Equal("test", m.Fields[0].DefaultValue[0].Value)
h.a.Equal(m.Fields[1].Kind, "Bool")
h.a.Len(m.Fields[1].DefaultValue, 1)
h.a.Equal("1", m.Fields[1].DefaultValue[0].Value)
}
func TestModuleFieldsDefaultValue(t *testing.T) {
var ns *types.Namespace
h := newHelper(t)
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
prep := func() {
h.clearModules()
ns = h.makeNamespace("some-namespace")
}
t.Run("boolean; true", func(t *testing.T) {
prep()
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "Boolean", Name: "boolean"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "boolean", "kind": "Boolean", "defaultValue": [{"name": "boolean", "value": "1"}] }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 1)
h.a.NotNil(m.Fields[0].DefaultValue)
h.a.Len(m.Fields[0].DefaultValue, 1)
h.a.Equal("1", m.Fields[0].DefaultValue[0].Value)
})
t.Run("boolean; false", func(t *testing.T) {
prep()
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "Boolean", Name: "boolean"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "boolean", "kind": "Boolean", "defaultValue": [{"name": "boolean", "value": ""}] }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 1)
h.a.NotNil(m.Fields[0].DefaultValue)
h.a.Len(m.Fields[0].DefaultValue, 1)
h.a.Equal("", m.Fields[0].DefaultValue[0].Value)
})
t.Run("boolean; undefined", func(t *testing.T) {
prep()
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "Boolean", Name: "boolean"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "boolean", "kind": "Boolean" }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 1)
h.a.Nil(m.Fields[0].DefaultValue)
h.a.Len(m.Fields[0].DefaultValue, 0)
})
t.Run("boolean; true; compact form", func(t *testing.T) {
prep()
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "Boolean", Name: "boolean"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "boolean", "kind": "Boolean", "defaultValue": [{"value": "1"}] }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 1)
h.a.NotNil(m.Fields[0].DefaultValue)
h.a.Len(m.Fields[0].DefaultValue, 1)
h.a.Equal("1", m.Fields[0].DefaultValue[0].Value)
})
t.Run("record; valid", func(t *testing.T) {
prep()
// Prep the related module
refM := h.makeModule(ns, "ref-mod", &types.ModuleField{
ID: id.Next(),
Kind: "String",
Name: "string",
})
rec := h.makeRecord(refM, &types.RecordValue{
Name: "string",
Value: "val",
})
// Prep the module and a field
m := h.makeModule(ns, "some-module", &types.ModuleField{
ID: id.Next(),
Kind: "String",
Name: "string",
})
// RBAC (make sure to allow record read)
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "record", "kind": "Record", "options": {"moduleID": "%d"}, "defaultValue": [{"value": "%d"}] }] }`, m.Name, f.ID, refM.ID, rec.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 1)
h.a.NotNil(m.Fields[0].DefaultValue)
h.a.Len(m.Fields[0].DefaultValue, 1)
h.a.Equal(strconv.FormatUint(rec.ID, 10), m.Fields[0].DefaultValue[0].Value)
})
}
func TestModuleFieldsUpdate_removed(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "a"}, &types.ModuleField{ID: id.Next(), Kind: "String", Name: "b"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "a", "kind": "String" }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 1)
h.a.NotNil(m.Fields[0].UpdatedAt)
h.a.Equal(m.Fields[0].Name, "a")
}
func TestModuleFieldsUpdate_removedHasRecords(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "a"}, &types.ModuleField{ID: id.Next(), Kind: "String", Name: "b"})
h.makeRecord(m, &types.RecordValue{Name: "a", Value: "va"}, &types.RecordValue{Name: "b", Value: "vb"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "a", "kind": "String" }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 1)
h.a.NotNil(m.Fields[0].UpdatedAt)
h.a.Equal(m.Fields[0].Name, "a")
}
func TestModuleFieldsUpdateExpressions(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "module.create")
ns := h.makeNamespace("some-namespace")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
var (
m = &types.Module{
NamespaceID: ns.ID,
}
f = &types.ModuleField{
ID: id.Next(),
Kind: "String",
Name: "existing",
Expressions: types.ModuleFieldExpr{
ValueExpr: `"foo"`,
Sanitizers: []string{"value"},
Validators: []types.ModuleFieldValidator{
{
Test: `value != ""`,
Error: "error",
},
},
DisableDefaultValidators: true,
Formatters: []string{`"foo"`},
DisableDefaultFormatters: false,
},
}
aux = struct{ Response types.Module }{}
)
m.Fields = append(m.Fields, f)
// create module
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/", ns.ID)).
JSON(helpers.JSON(m)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&aux)
m = &aux.Response
h.a.NotEmpty(m.Fields)
f = m.Fields.FindByName("existing")
h.a.NotNil(f)
h.a.Equal(`"foo"`, f.Expressions.ValueExpr)
h.a.NotEmpty(f.Expressions.Sanitizers)
h.a.NotEmpty(f.Expressions.Validators)
h.a.True(f.Expressions.DisableDefaultValidators)
h.a.NotEmpty(f.Expressions.Formatters)
h.a.False(f.Expressions.DisableDefaultFormatters)
f.Expressions = types.ModuleFieldExpr{
ValueExpr: `"bar"`,
Sanitizers: []string{`""`},
Validators: []types.ModuleFieldValidator{
{
Test: `value == ""`,
Error: "foo",
},
},
DisableDefaultValidators: false,
Formatters: []string{`"bar"`},
DisableDefaultFormatters: true,
}
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(helpers.JSON(m)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&aux)
h.a.NotEmpty(aux.Response.Fields)
f = aux.Response.Fields.FindByName("existing")
h.a.NotNil(f)
h.a.Equal(`"bar"`, f.Expressions.ValueExpr)
h.a.NotEmpty(f.Expressions.Sanitizers)
h.a.NotEmpty(f.Expressions.Validators)
h.a.False(f.Expressions.DisableDefaultValidators)
h.a.NotEmpty(f.Expressions.Formatters)
h.a.True(f.Expressions.DisableDefaultFormatters)
}
func TestModuleFieldsPreventUpdate_ifRecordExists(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module", &types.ModuleField{ID: id.Next(), Kind: "String", Name: "existing"})
h.makeRecord(m, &types.RecordValue{Name: "existing", Value: "value"})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
f := m.Fields[0]
fjs := fmt.Sprintf(`{ "name": "%s", "fields": [{ "fieldID": "%d", "name": "existing_edited", "kind": "Number" }, { "name": "new", "kind": "DateTime" }] }`, m.Name, f.ID)
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
JSON(fjs).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
m = h.lookupModuleByID(m.ID)
h.a.NotNil(m)
h.a.NotNil(m.Fields)
h.a.Len(m.Fields, 2)
h.a.NotNil(m.Fields[0].UpdatedAt)
h.a.Equal(m.Fields[0].Name, "existing")
h.a.Equal(m.Fields[0].Kind, "String")
h.a.Nil(m.Fields[1].UpdatedAt)
h.a.Equal(m.Fields[1].Name, "new")
h.a.Equal(m.Fields[1].Kind, "DateTime")
}
func TestModuleDeleteForbidden(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
m := h.makeModule(ns, "some-module")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, m.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("module.errors.notAllowedToDelete")).
End()
}
func TestModuleDelete(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "delete")
ns := h.makeNamespace("some-namespace")
res := h.makeModule(ns, "some-module")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, res.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupModuleByID(res.ID)
h.a.NotNil(res.DeletedAt)
}
func TestModuleLabels(t *testing.T) {
h := newHelper(t)
h.clearModules()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "modules.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "module.create")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "delete")
var (
ns = h.makeNamespace("some-namespace")
fieldID, ID uint64
)
t.Run("create", func(t *testing.T) {
var (
req = require.New(t)
payload = &types.Module{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d/module/", ns.ID),
types.Module{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "bar"}, "bar": {Val: "42"}}},
payload,
)
req.NotZero(payload.ID)
h.a.Equal(payload.Labels["foo"].Val, "bar",
"labels must contain foo with value bar")
h.a.Equal(payload.Labels["bar"].Val, "42",
"labels must contain bar with value 42")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
ID = payload.ID
})
t.Run("update", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
payload = &types.Module{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d/module/%d", ns.ID, ID),
types.Module{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "baz"}, "baz": {Val: "123"}}},
payload,
)
req.NotZero(payload.ID)
req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels")
req.Equal(payload.Labels["foo"].Val, "baz",
"labels must contain foo with value baz")
req.NotContains(payload.Labels, "bar",
"labels must not contain bar")
req.Equal(payload.Labels["baz"].Val, "123",
"labels must contain baz with value 123")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
})
t.Run("search", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
set = types.ModuleSet{}
)
helpers.SearchWithLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d/module/", ns.ID),
&set,
url.Values{"labels": []string{"baz=123"}},
)
req.NotEmpty(set)
req.NotNil(set.FindByID(ID).Labels)
})
t.Run("field create", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
ctx = context.Background()
req = require.New(t)
mod, err = store.LookupComposeModuleByID(ctx, service.DefaultStore, ID)
payload = struct{ Response *types.Module }{}
)
req.NoError(err)
req.NotNil(mod)
mod.Fields = append(mod.Fields, &types.ModuleField{
Kind: "String",
Name: "labeled",
Label: "",
Labels: map[string]labelTypes.LabelValue{"fldfoo": {Val: "fldbar"}},
})
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, ID)).
JSON(helpers.JSON(mod)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.fields[0].labels.fldfoo`, "fldbar")).
End().
JSON(&payload)
fieldID = payload.Response.Fields[0].ID
})
t.Run("field update", func(t *testing.T) {
if fieldID == 0 {
t.Skip("label/field create test not ran")
}
var (
ctx = context.Background()
req = require.New(t)
mod, err = store.LookupComposeModuleByID(ctx, service.DefaultStore, ID)
)
req.NoError(err)
req.NotNil(mod)
mod.Fields = append(mod.Fields, &types.ModuleField{
ID: fieldID,
Kind: "String",
Name: "labeled",
Label: "",
Labels: map[string]labelTypes.LabelValue{"fldfoo": {Val: "fldbaz"}},
})
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, ID)).
JSON(helpers.JSON(mod)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.fields[0].labels.fldfoo`, "fldbaz")).
End()
})
}

View File

@@ -0,0 +1,171 @@
package compose
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/store"
sysTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/spf13/cast"
)
func fetchEntireNamespace(ctx context.Context, s store.Storer, slug string) (ns *types.Namespace, mm types.ModuleSet, pp types.PageSet, cc types.ChartSet, slg string, err error) {
slg = slug
ns, err = store.LookupComposeNamespaceBySlug(ctx, s, slug)
if err != nil {
return
}
mm, _, err = store.SearchComposeModules(ctx, s, types.ModuleFilter{NamespaceID: ns.ID})
if err != nil {
return
}
for i := 0; i < len(mm); i++ {
mm[i].Fields, _, err = store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{ModuleID: []uint64{mm[i].ID}})
if err != nil {
return
}
}
pp, _, err = store.SearchComposePages(ctx, s, types.PageFilter{NamespaceID: ns.ID})
if err != nil {
return
}
cc, _, err = store.SearchComposeCharts(ctx, s, types.ChartFilter{NamespaceID: ns.ID})
if err != nil {
return
}
return
}
func findModuleByHandle(mm types.ModuleSet, h string) *types.Module {
for _, m := range mm {
if m.Handle == h {
return m
}
}
return nil
}
func findChartByHandle(cc types.ChartSet, h string) *types.Chart {
for _, m := range cc {
if m.Handle == h {
return m
}
}
return nil
}
func findPageByHandle(pp types.PageSet, h string) *types.Page {
for _, m := range pp {
if m.Handle == h {
return m
}
}
return nil
}
func Test_namespace_duplicate(t *testing.T) {
ctx, h, s := setup(t)
loadScenario(ctx, defStore, t, h)
ns, _, _, _, _, err := fetchEntireNamespace(ctx, s, "ns1")
h.a.NoError(err)
helpers.AllowMe(h, types.ComponentRbacResource(), "namespace.create")
helpers.AllowMe(h, types.ComponentRbacResource(), "module.create")
helpers.AllowMe(h, types.ComponentRbacResource(), "page.create")
helpers.AllowMe(h, types.ComponentRbacResource(), "chart.create")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "modules.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "charts.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "pages.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "modules.export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "charts.export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
helpers.AllowMe(h, types.ChartRbacResource(0, 0), "read")
helpers.AllowMe(h, sysTypes.ComponentRbacResource(), "roles.search")
helpers.AllowMe(h, sysTypes.RoleRbacResource(0), "read")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/clone", ns.ID)).
JSON(`{ "slug": "cloned", "name": "cloned name" }`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
usedIDs := make(map[uint64]bool)
checkID := func(h helper, id uint64) {
if usedIDs[id] {
h.a.FailNow(fmt.Sprintf("the ID is not unique across cloned resources: %d", id))
}
usedIDs[id] = true
}
checker := func(ns *types.Namespace, mm types.ModuleSet, pp types.PageSet, cc types.ChartSet, slug string, err error) {
h.a.NoError(err)
// NS
h.a.Equal(slug, ns.Slug)
// Modules
mod1 := findModuleByHandle(mm, "mod1")
h.a.NotNil(mod1)
checkID(h, mod1.ID)
mod2 := findModuleByHandle(mm, "mod2")
h.a.NotNil(mod2)
checkID(h, mod2.ID)
mod3 := findModuleByHandle(mm, "mod3")
h.a.NotNil(mod3)
checkID(h, mod3.ID)
h.a.Len(mod1.Fields, 4)
h.a.Equal("Record", mod1.Fields.FindByName("f4").Kind)
h.a.Equal(mod2.ID, mod1.Fields.FindByName("f4").Options.UInt64("moduleID"))
h.a.Len(mod2.Fields, 4)
h.a.Equal("Record", mod2.Fields.FindByName("f_ref_self").Kind)
h.a.Equal(mod2.ID, mod2.Fields.FindByName("f_ref_self").Options.UInt64("moduleID"))
h.a.Equal("Record", mod2.Fields.FindByName("f2").Kind)
h.a.Equal(mod3.ID, mod2.Fields.FindByName("f2").Options.UInt64("moduleID"))
h.a.Len(mod3.Fields, 1)
// Charts
chr1 := findChartByHandle(cc, "chr1")
checkID(h, chr1.ID)
h.a.NotNil(chr1)
h.a.Len(chr1.Config.Reports, 1)
h.a.Equal(mod1.ID, chr1.Config.Reports[0].ModuleID)
// Pages
pg1 := findPageByHandle(pp, "pg1")
checkID(h, pg1.ID)
h.a.NotNil(pg1)
rpg2 := findPageByHandle(pp, "rpg2")
checkID(h, rpg2.ID)
h.a.NotNil(rpg2)
h.a.Len(pg1.Blocks, 3)
h.a.Equal("RecordList", pg1.Blocks[1].Kind)
h.a.Equal(mod1.ID, cast.ToUint64(pg1.Blocks[1].Options["moduleID"]))
h.a.Equal("Chart", pg1.Blocks[2].Kind)
h.a.Equal(chr1.ID, cast.ToUint64(pg1.Blocks[2].Options["chartID"]))
h.a.Equal(rpg2.ModuleID, mod1.ID)
}
checker(fetchEntireNamespace(ctx, s, "ns1"))
checker(fetchEntireNamespace(ctx, s, "cloned"))
}

View File

@@ -0,0 +1,36 @@
package compose
import (
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
)
func Test_namespace_export_automation(t *testing.T) {
ctx, h, s := setup(t)
loadScenario(ctx, defStore, t, h)
grantImportExport(h)
ns, _, _, _, _, err := fetchEntireNamespace(ctx, s, "ns1")
h.a.NoError(err)
helpers.AllowMe(h, types.NamespaceRbacResource(0), "export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "modules.export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "charts.export")
arch := namespaceExportSafe(t, h, ns.ID)
sessionID := namespaceImportInitSafe(t, h, arch)
ns, _, pp, _ := namespaceImportRun(ctx, s, t, h, sessionID, "imported", "imported")
h.a.Equal("imported", ns.Slug)
h.a.NotEqual(0, ns.ID)
p := pp.FindByHandle("pg1")
h.a.Len(p.Blocks, 2)
for _, b := range p.Blocks {
h.a.NotEqual("Automation", b.Kind)
}
cleanup(t)
}

View File

@@ -0,0 +1,34 @@
package compose
import (
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
)
func Test_namespace_export_empty(t *testing.T) {
ctx, h, s := setup(t)
loadScenario(ctx, defStore, t, h)
grantImportExport(h)
ns, _, _, _, _, err := fetchEntireNamespace(ctx, s, "ns1")
h.a.NoError(err)
helpers.AllowMe(h, types.NamespaceRbacResource(0), "export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "modules.export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "charts.export")
arch := namespaceExportSafe(t, h, ns.ID)
sessionID := namespaceImportInitSafe(t, h, arch)
ns, mm, pp, cc := namespaceImportRun(ctx, s, t, h, sessionID, "imported", "imported")
h.a.Equal("imported", ns.Slug)
h.a.NotEqual(0, ns.ID)
h.a.Len(mm, 0)
h.a.Len(pp, 0)
h.a.Len(cc, 0)
cleanup(t)
}

View File

@@ -0,0 +1,26 @@
package compose
import (
"testing"
"github.com/cortezaproject/corteza/server/store"
)
func Test_namespace_export_missing_res(t *testing.T) {
t.Skip("skipping due to no longer erroring out; to be addressed")
ctx, h, s := setup(t)
loadScenario(ctx, defStore, t, h)
grantImportExport(h)
ns, mm, _, _, _, err := fetchEntireNamespace(ctx, s, "ns1")
h.a.NoError(err)
// Removeing one of the resources
h.a.NoError(store.DeleteComposeModuleByID(ctx, s, mm.FindByHandle("mod2").ID))
_, err = namespaceExport(t, h, ns.ID)
h.a.Error(err)
cleanup(t)
}

View File

@@ -0,0 +1,60 @@
package compose
import (
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
)
func Test_namespace_export_simple(t *testing.T) {
ctx, h, s := setup(t)
loadScenario(ctx, defStore, t, h)
grantImportExport(h)
ns, _, _, _, _, err := fetchEntireNamespace(ctx, s, "ns1")
h.a.NoError(err)
helpers.AllowMe(h, types.NamespaceRbacResource(0), "export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "modules.export")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "charts.export")
arch := namespaceExportSafe(t, h, ns.ID)
sessionID := namespaceImportInitSafe(t, h, arch)
ns, mm, pp, cc := namespaceImportRun(ctx, s, t, h, sessionID, "imported", "imported")
h.a.Equal("imported", ns.Slug)
h.a.NotEqual(0, ns.ID)
h.a.Len(mm, 3)
exp := map[string]bool{"mod1": true, "mod2": true, "mod3": true}
for i, m := range mm {
h.a.True(exp[m.Handle])
if i > 0 {
h.a.NotEqual(m.Handle, mm[i-1].Handle)
}
}
h.a.Len(pp, 3)
exp = map[string]bool{
"pg1": true,
"rpg2": true,
"pg2": true,
}
for i, p := range pp {
h.a.True(exp[p.Handle])
if i > 0 {
h.a.NotEqual(p.Handle, mm[i-1].Handle)
}
}
parent := pp.FindByHandle("rpg2")
child := pp.FindByHandle("pg2")
h.a.Equal(child.SelfID, parent.ID)
h.a.Len(cc, 1)
h.a.Equal("chr1", cc[0].Handle)
cleanup(t)
}

View File

@@ -0,0 +1,18 @@
package compose
import (
"testing"
)
func Test_namespace_export_unexisting(t *testing.T) {
ctx, h, s := setup(t)
grantImportExport(h)
_ = ctx
_ = s
_, err := namespaceExport(t, h, 42)
h.a.Error(err)
h.a.Contains(err.Error(), "namespace does not exist")
cleanup(t)
}

View File

@@ -0,0 +1,24 @@
package compose
import (
"testing"
)
func Test_namespace_import(t *testing.T) {
t.Run("nested", func(_ *testing.T) {
_, h, _ := setup(t)
grantImportExport(h)
sessionID := namespaceImportInitPathSafe(t, h, "nested.zip")
h.a.NotEqual(0, sessionID)
})
t.Run("no namespace", func(_ *testing.T) {
_, h, _ := setup(t)
grantImportExport(h)
_, err := namespaceImportInitPath(t, h, "no-ns.zip")
h.a.Error(err)
h.a.Contains(err.Error(), "namespace.errors.importMissingNamespace")
})
}

View File

@@ -0,0 +1,293 @@
package compose
import (
"context"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/id"
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/pkg/rand"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
"github.com/stretchr/testify/require"
)
func (h helper) clearNamespaces() {
h.noError(store.TruncateComposeAttachments(context.Background(), service.DefaultStore))
h.noError(store.TruncateComposeNamespaces(context.Background(), service.DefaultStore))
}
func (h helper) makeNamespace(name string) *types.Namespace {
ns := &types.Namespace{Name: name, Slug: name}
ns.ID = id.Next()
ns.CreatedAt = time.Now()
h.noError(store.CreateComposeNamespace(context.Background(), service.DefaultStore, ns))
return ns
}
func (h helper) lookupNamespaceByID(ID uint64) *types.Namespace {
ns, err := store.LookupComposeNamespaceByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return ns
}
func TestNamespaceRead(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
ns := h.makeNamespace("some-namespace")
helpers.AllowMe(h, types.ComponentRbacResource(), "namespaces.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.name`, ns.Name)).
Assert(jsonpath.Equal(`$.response.namespaceID`, fmt.Sprintf("%d", ns.ID))).
End()
}
func TestNamespaceReadByHandle(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
helpers.AllowMe(h, types.ComponentRbacResource(), "namespaces.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
ns := h.makeNamespace("some-namespace-" + string(rand.Bytes(20)))
nsbh, err := service.DefaultNamespace.FindByHandle(h.secCtx(), ns.Slug)
h.noError(err)
h.a.NotNil(nsbh)
h.a.Equal(nsbh.ID, ns.ID)
h.a.Equal(nsbh.Slug, ns.Slug)
}
func TestNamespaceList(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
helpers.AllowMe(h, types.ComponentRbacResource(), "namespaces.search")
h.makeNamespace("ns1")
h.makeNamespace("ns2")
h.apiInit().
Get("/namespace/").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestNamespaceList_filterForbidden(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
h.makeNamespace("namespace")
f := h.makeNamespace("namespace_forbidden")
helpers.AllowMe(h, types.ComponentRbacResource(), "namespaces.search")
helpers.DenyMe(h, types.NamespaceRbacResource(f.ID), "read")
h.apiInit().
Get("/namespace/").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.NotPresent(`$.response.set[? @.name=="namespace_forbidden"]`)).
End()
}
func TestNamespaceCreateForbidden(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
helpers.AllowMe(h, types.ComponentRbacResource(), "namespaces.search")
h.apiInit().
Post("/namespace/").
Header("Accept", "application/json").
FormData("name", "some-namespace").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("namespace.errors.notAllowedToCreate")).
End()
}
func TestNamespaceCreate(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
helpers.AllowMe(h, types.ComponentRbacResource(), "namespace.create")
h.apiInit().
Post("/namespace/").
FormData("name", "some-namespace").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestNamespaceUpdateForbidden(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
ns := h.makeNamespace("some-namespace")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d", ns.ID)).
Header("Accept", "application/json").
FormData("name", "changed-name").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("namespace.errors.notAllowedToUpdate")).
End()
}
func TestNamespaceUpdate(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
ns := h.makeNamespace("some-namespace")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "update")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d", ns.ID)).
FormData("name", "changed-name").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ns = h.lookupNamespaceByID(ns.ID)
h.a.NotNil(ns)
h.a.Equal("changed-name", ns.Name)
}
func TestNamespaceDeleteForbidden(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
ns := h.makeNamespace("some-namespace")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.DenyMe(h, types.NamespaceRbacResource(0), "delete")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d", ns.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("namespace.errors.notAllowedToDelete")).
End()
}
func TestNamespaceDelete(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "delete")
ns := h.makeNamespace("some-namespace")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ns = h.lookupNamespaceByID(ns.ID)
h.a.NotNil(ns.DeletedAt)
}
func TestNamespaceLabels(t *testing.T) {
h := newHelper(t)
h.clearNamespaces()
helpers.AllowMe(h, types.ComponentRbacResource(), "namespaces.search")
helpers.AllowMe(h, types.ComponentRbacResource(), "namespace.create")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "delete", "update")
var (
ID uint64
)
t.Run("create", func(t *testing.T) {
var (
req = require.New(t)
payload = &types.Namespace{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
"/namespace/",
types.Namespace{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "bar"}, "bar": {Val: "42"}}},
payload,
)
req.NotZero(payload.ID)
h.a.Equal(payload.Labels["foo"].Val, "bar",
"labels must contain foo with value bar")
h.a.Equal(payload.Labels["bar"].Val, "42",
"labels must contain bar with value 42")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
ID = payload.ID
})
t.Run("update", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
payload = &types.Namespace{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d", ID),
&types.Namespace{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "baz"}, "baz": {Val: "123"}}},
payload,
)
req.NotZero(payload.ID)
req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels")
req.Equal(payload.Labels["foo"].Val, "baz",
"labels must contain foo with value baz")
req.NotContains(payload.Labels, "bar",
"labels must not contain bar")
req.Equal(payload.Labels["baz"].Val, "123",
"labels must contain baz with value 123")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
})
t.Run("search", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
set = types.NamespaceSet{}
)
helpers.SearchWithLabelsViaAPI(h.apiInit(), t, "/namespace/", &set, url.Values{"labels": []string{"baz=123"}})
req.NotEmpty(set)
req.NotNil(set.FindByID(ID))
req.NotNil(set.FindByID(ID).Labels)
})
}

View File

@@ -0,0 +1,34 @@
package compose
import (
"encoding/json"
"github.com/cortezaproject/corteza/server/compose/rest/request"
sqlxTypes "github.com/jmoiron/sqlx/types"
"github.com/steinfletcher/apitest"
"net/http"
"testing"
)
func (h helper) apiSendEmailNotification(req request.NotificationEmailSend) *apitest.Response {
payload, err := json.Marshal(req)
h.noError(err)
return h.apiInit().
Post("/notification/email").
JSON(string(payload)).
Expect(h.t).
Status(http.StatusOK)
}
func TestEmailNotification(t *testing.T) {
t.Skip("we need smtp server mock")
h := newHelper(t)
h.apiSendEmailNotification(request.NotificationEmailSend{
To: []string{"foo+to@test.tld"},
Cc: []string{"foo+cc1@test.tld", "foo+cc2@test.tld"},
Subject: "Subject!",
Content: sqlxTypes.JSONText(`{}`),
RemoteAttachments: []string{"file1", "file2"},
}).End()
}

View File

@@ -0,0 +1,224 @@
package compose
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
)
func (h helper) clearPageLayouts() {
h.clearNamespaces()
h.noError(store.TruncateComposePageLayouts(context.Background(), service.DefaultStore))
}
func (h helper) repoMakePageLayout(ns *types.Namespace, pg *types.Page, title string) *types.PageLayout {
res := &types.PageLayout{
ID: id.Next(),
CreatedAt: time.Now(),
Meta: types.PageLayoutMeta{
Title: title,
},
NamespaceID: ns.ID,
PageID: pg.ID,
}
h.noError(store.CreateComposePageLayout(context.Background(), service.DefaultStore, res))
return res
}
func (h helper) lookupPageLayoutByID(ID uint64) *types.PageLayout {
res, err := store.LookupComposePageLayoutByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return res
}
func TestPageLayoutRead(t *testing.T) {
h := newHelper(t)
h.clearPageLayouts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.PageLayoutRbacResource(0, 0, 0), "read")
ns := h.makeNamespace("some-namespace")
pg := h.repoMakePage(ns, "some-page")
ly := h.repoMakePageLayout(ns, pg, "some-page-layout")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/page/%d/layout/%d", ns.ID, pg.ID, ly.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.meta.title`, ly.Meta.Title)).
Assert(jsonpath.Equal(`$.response.pageLayoutID`, fmt.Sprintf("%d", ly.ID))).
End()
}
func TestPageLayoutList_filterForbidden(t *testing.T) {
h := newHelper(t)
h.clearPageLayouts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read", "page-layouts.search")
ns := h.makeNamespace("some-namespace")
pg := h.repoMakePage(ns, "some-page")
h.repoMakePageLayout(ns, pg, "page-layout")
ly := h.repoMakePageLayout(ns, pg, "page-layout_forbidden")
helpers.DenyMe(h, types.PageLayoutRbacResource(ly.NamespaceID, ly.PageID, ly.ID), "read")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/page/%d/layout/", ns.ID, pg.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.NotPresent(`$.response.set[? @.meta.title=="page-layout_forbidden"]`)).
End()
}
func TestPageLayoutCreateForbidden(t *testing.T) {
h := newHelper(t)
h.clearPageLayouts()
ns := h.makeNamespace("some-namespace")
pg := h.repoMakePage(ns, "some-page")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/%d/layout/", ns.ID, pg.ID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{
"meta": {"title": "some-page-layout"}
}`)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("page-layout.errors.notAllowedToCreate")).
End()
}
func TestPageLayoutCreate(t *testing.T) {
h := newHelper(t)
h.clearPageLayouts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "page-layout.create")
ns := h.makeNamespace("some-namespace")
pg := h.repoMakePage(ns, "some-page")
rsp := struct {
Response *types.PageLayout `json:"response"`
}{}
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/%d/layout/", ns.ID, pg.ID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{
"meta": {"title": "some-page-layout"}
}`)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&rsp)
res := h.lookupPageLayoutByID(rsp.Response.ID)
h.a.NotNil(res)
h.a.Equal("some-page-layout", res.Meta.Title)
}
func TestPageLayoutUpdateForbidden(t *testing.T) {
h := newHelper(t)
h.clearPageLayouts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
ns := h.makeNamespace("some-namespace")
pg := h.repoMakePage(ns, "some-page")
ly := h.repoMakePageLayout(ns, pg, "some-page-layout")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/%d/layout/%d", ns.ID, pg.ID, ly.ID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{
"meta": {"title": "changed-name"}
}`)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("page-layout.errors.notAllowedToUpdate")).
End()
}
func TestPageLayoutUpdate(t *testing.T) {
h := newHelper(t)
h.clearPageLayouts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
ns := h.makeNamespace("some-namespace")
pg := h.repoMakePage(ns, "some-page")
ly := h.repoMakePageLayout(ns, pg, "some-page-layout")
helpers.AllowMe(h, types.PageLayoutRbacResource(0, 0, 0), "update")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/%d/layout/%d", ns.ID, pg.ID, ly.ID)).
JSON(fmt.Sprintf(`{
"meta": {"title": "changed-name"}
}`)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ly = h.lookupPageLayoutByID(ly.ID)
h.a.NotNil(ly)
h.a.Equal("changed-name", ly.Meta.Title)
}
func TestPageLayoutDeleteForbidden(t *testing.T) {
h := newHelper(t)
h.clearPageLayouts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.PageLayoutRbacResource(0, 0, 0), "read")
ns := h.makeNamespace("some-namespace")
pg := h.repoMakePage(ns, "some-page")
ly := h.repoMakePageLayout(ns, pg, "some-page-layout")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/page/%d/layout/%d", ns.ID, pg.ID, ly.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("page-layout.errors.notAllowedToDelete")).
End()
}
func TestPageLayoutDelete(t *testing.T) {
h := newHelper(t)
h.clearPageLayouts()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.PageLayoutRbacResource(0, 0, 0), "read")
helpers.AllowMe(h, types.PageLayoutRbacResource(0, 0, 0), "delete")
ns := h.makeNamespace("some-namespace")
pg := h.repoMakePage(ns, "some-page")
ly := h.repoMakePageLayout(ns, pg, "some-page-layout")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/page/%d/layout/%d", ns.ID, pg.ID, ly.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
ly = h.lookupPageLayoutByID(ly.ID)
h.a.NotNil(ly.DeletedAt)
}

View File

@@ -0,0 +1,532 @@
package compose
import (
"bytes"
"context"
"fmt"
"net/http"
"net/url"
"os"
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/service"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/id"
labelTypes "github.com/cortezaproject/corteza/server/pkg/label/types"
"github.com/cortezaproject/corteza/server/store"
systemService "github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
"github.com/stretchr/testify/require"
)
func (h helper) clearPages() {
h.clearNamespaces()
h.noError(store.TruncateComposePages(context.Background(), service.DefaultStore))
}
func (h helper) repoMakePage(ns *types.Namespace, name string) *types.Page {
res := &types.Page{
ID: id.Next(),
CreatedAt: time.Now(),
Meta: types.PageMeta{},
Title: name,
NamespaceID: ns.ID,
}
res.Blocks = types.PageBlocks{
{BlockID: 1},
{BlockID: 2},
}
res.Config.NavItem.Icon = &types.PageConfigIcon{
Type: "type",
Src: "src",
Style: map[string]string{"sty": "le"},
}
h.noError(store.CreateComposePage(context.Background(), service.DefaultStore, res))
return res
}
func (h helper) repoMakeWeightedPage(ns *types.Namespace, name string, weight int) *types.Page {
res := &types.Page{
ID: id.Next(),
CreatedAt: time.Now(),
Meta: types.PageMeta{},
Title: name,
NamespaceID: ns.ID,
Weight: weight,
}
h.noError(store.CreateComposePage(context.Background(), service.DefaultStore, res))
return res
}
func (h helper) lookupPageByID(ID uint64) *types.Page {
res, err := store.LookupComposePageByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return res
}
func TestPageRead(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
m := h.repoMakePage(ns, "some-page")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/page/%d", ns.ID, m.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.title`, m.Title)).
Assert(jsonpath.Equal(`$.response.pageID`, fmt.Sprintf("%d", m.ID))).
Assert(jsonpath.Equal(`$.response.config.navItem.icon.src`, `src`)).
Assert(jsonpath.Len(`$.response.blocks`, 2)).
End()
}
func TestPageReadByHandle(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
c := h.repoMakePage(ns, "some-page")
cbh, err := service.DefaultPage.FindByHandle(h.secCtx(), ns.ID, c.Handle)
h.noError(err)
h.a.NotNil(cbh)
h.a.Equal(cbh.ID, c.ID)
h.a.Equal(cbh.Handle, c.Handle)
}
func TestPageList(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
ns := h.makeNamespace("some-namespace")
h.repoMakePage(ns, "app")
h.repoMakePage(ns, "app")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/page/", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestPageList_filterForbidden(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
ns := h.makeNamespace("some-namespace")
h.repoMakePage(ns, "page")
f := h.repoMakePage(ns, "page_forbidden")
helpers.DenyMe(h, types.PageRbacResource(f.NamespaceID, f.ID), "read")
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/page/", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.NotPresent(`$.response.set[? @.title=="page_forbidden"]`)).
End()
}
func TestPageCreateForbidden(t *testing.T) {
h := newHelper(t)
h.clearPages()
ns := h.makeNamespace("some-namespace")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/", ns.ID)).
Header("Accept", "application/json").
FormData("title", "some-page").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("page.errors.notAllowedToCreate")).
End()
}
func TestPageCreate(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "page.create")
ns := h.makeNamespace("some-namespace")
rsp := struct {
Response *types.Page `json:"response"`
}{}
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/", ns.ID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{
"title": "some-page",
"config":{"navItem":{"icon":{"src":"my-icon"}}},
"blocks":[{"blockID": "1"}]
}`)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End().
JSON(&rsp)
res := h.lookupPageByID(rsp.Response.ID)
h.a.NotNil(res)
h.a.Equal("some-page", res.Title)
h.a.Len(res.Blocks, 1)
h.a.NotNil(res.Config.NavItem.Icon)
h.a.Equal("my-icon", res.Config.NavItem.Icon.Src)
}
func TestPageUpdateForbidden(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
ns := h.makeNamespace("some-namespace")
m := h.repoMakePage(ns, "some-page")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/%d", ns.ID, m.ID)).
Header("Accept", "application/json").
FormData("title", "changed-name").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("page.errors.notAllowedToUpdate")).
End()
}
func TestPageUpdate(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
ns := h.makeNamespace("some-namespace")
res := h.repoMakePage(ns, "some-page")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "update")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/%d", ns.ID, res.ID)).
FormData("title", "changed-name").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupPageByID(res.ID)
h.a.NotNil(res)
h.a.Equal("changed-name", res.Title)
}
func TestPageUpdateWithBlocksAndConfig(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
ns := h.makeNamespace("some-namespace")
res := h.repoMakePage(ns, "some-page")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "update")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/%d", ns.ID, res.ID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{
"title": "changed-name",
"config":{"navItem":{"icon":{"src":"my-icon"}}},
"blocks":[{"blockID": "1"}]
}`)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupPageByID(res.ID)
h.a.NotNil(res)
h.a.Equal("changed-name", res.Title)
h.a.NotNil(res.Config.NavItem.Icon)
h.a.Equal("my-icon", res.Config.NavItem.Icon.Src)
h.a.Len(res.Blocks, 1)
}
func TestPageReorder(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.PageRbacResource(0, 0), "update")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
ns := h.makeNamespace("some-namespace")
res := h.repoMakePage(ns, "some-page")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/page/%d/reorder", ns.ID, res.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestPageDeleteForbidden(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
m := h.repoMakePage(ns, "some-page")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/page/%d", ns.ID, m.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("page.errors.notAllowedToDelete")).
End()
}
func TestPageDelete(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "delete")
ns := h.makeNamespace("some-namespace")
res := h.repoMakePage(ns, "some-page")
h.apiInit().
Delete(fmt.Sprintf("/namespace/%d/page/%d", ns.ID, res.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupPageByID(res.ID)
h.a.NotNil(res.DeletedAt)
}
func TestPageTreeRead(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
ns := h.makeNamespace("some-namespace")
h.repoMakeWeightedPage(ns, "p1", 1)
h.repoMakeWeightedPage(ns, "p4", 4)
h.repoMakeWeightedPage(ns, "p3", 3)
h.repoMakeWeightedPage(ns, "p2", 2)
h.apiInit().
Get(fmt.Sprintf("/namespace/%d/page/tree", ns.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response[0].title`, "p1")).
Assert(jsonpath.Equal(`$.response[1].title`, "p2")).
Assert(jsonpath.Equal(`$.response[2].title`, "p3")).
Assert(jsonpath.Equal(`$.response[3].title`, "p4")).
End()
}
func TestPageAttachment(t *testing.T) {
h := newHelper(t)
h.clearPages()
ns := h.makeNamespace("page attachment testing namespace")
page := h.repoMakePage(ns, "some-page")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read", "update")
xxlBlob := bytes.Repeat([]byte("0"), 1_000_001)
testImgFh, err := os.ReadFile("./testdata/test.png")
h.noError(err)
defer func() {
// reset settings after we're done
systemService.CurrentSettings.Compose.Page.Attachments.MaxSize = 0
systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes = nil
}()
// one megabyte limit
systemService.CurrentSettings.Compose.Page.Attachments.MaxSize = 1
systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes = []string{}
cc := []struct {
name string
file []byte
fname string
mtype string
form map[string]string
test func(*http.Response, *http.Request) error
}{
{
"empty file",
[]byte(""),
"empty",
"plain/text",
map[string]string{},
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
},
{
"no file",
nil,
"empty",
"plain/text",
map[string]string{},
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
},
{
"valid upload, no constraints",
[]byte("."),
"dot",
"plain/text",
map[string]string{},
helpers.AssertNoErrors,
},
{
"global max size - over sized",
xxlBlob,
"numbers",
"plain/text",
map[string]string{},
helpers.AssertError("attachment.errors.tooLarge"),
},
{
"global mimetype - invalid",
testImgFh,
"numbers.gif",
"image/gif",
map[string]string{},
helpers.AssertError("attachment.errors.failedToProcessImage"),
},
}
for _, c := range cc {
t.Run(c.name, func(t *testing.T) {
h.t = t
helpers.InitFileUpload(t, h.apiInit(),
fmt.Sprintf("/namespace/%d/page/%d/attachment", page.NamespaceID, page.ID),
c.form,
c.file,
c.fname,
c.mtype,
).
Status(http.StatusOK).
Assert(c.test).
End()
})
}
}
func TestPageLabels(t *testing.T) {
h := newHelper(t)
h.clearPages()
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read", "pages.search")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "page.create")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "update")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "delete")
var (
ns = h.makeNamespace("some-namespace")
ID uint64
)
t.Run("create", func(t *testing.T) {
var (
req = require.New(t)
payload = &types.Page{}
)
helpers.SetLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d/page/", ns.ID),
types.Page{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "bar"}, "bar": {Val: "42"}}},
payload,
)
req.NotZero(payload.ID)
h.a.Equal(payload.Labels["foo"].Val, "bar",
"labels must contain foo with value bar")
h.a.Equal(payload.Labels["bar"].Val, "42",
"labels must contain bar with value 42")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
ID = payload.ID
})
t.Run("update", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
payload = &types.Page{}
)
helpers.SetLabelsViaAPI(h.apiInit(),
t,
fmt.Sprintf("/namespace/%d/page/%d", ns.ID, ID),
types.Page{Labels: map[string]labelTypes.LabelValue{"foo": {Val: "baz"}, "baz": {Val: "123"}}},
payload,
)
req.NotZero(payload.ID)
req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels")
req.Equal(payload.Labels["foo"].Val, "baz",
"labels must contain foo with value baz")
req.NotContains(payload.Labels, "bar",
"labels must not contain bar")
req.Equal(payload.Labels["baz"].Val, "123",
"labels must contain baz with value 123")
req.Equal(helpers.LabelsToStrings(payload.Labels), helpers.LabelsToStrings(helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID)),
"response must match stored labels")
})
t.Run("search", func(t *testing.T) {
if ID == 0 {
t.Skip("label/create test not ran")
}
var (
req = require.New(t)
set = types.PageSet{}
)
helpers.SearchWithLabelsViaAPI(h.apiInit(), t,
fmt.Sprintf("/namespace/%d/page/", ns.ID),
&set,
url.Values{"labels": []string{"baz=123"}},
)
req.NotEmpty(set)
req.NotNil(set.FindByID(ID))
req.NotNil(set.FindByID(ID).Labels)
})
}

View File

@@ -0,0 +1,121 @@
package compose
import (
"fmt"
"net/http"
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/steinfletcher/apitest-jsonpath"
)
func TestPermissionsEffective(t *testing.T) {
h := newHelper(t)
helpers.DenyMe(h, types.ComponentRbacResource(), "namespace.create")
h.apiInit().
Get("/permissions/effective").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestPermissionsList(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
h.apiInit().
Debug().
Get("/permissions/").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present(fmt.Sprintf(`$.response[? @.type=="%s"]`, types.ComponentResourceType))).
End()
}
func TestPermissionsRead(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
helpers.DenyMe(h, types.ComponentRbacResource(), "namespace.create")
h.apiInit().
Get(fmt.Sprintf("/permissions/%d/rules", h.roleID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestPermissionsUpdate(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
h.apiInit().
Patch(fmt.Sprintf("/permissions/%d/rules", h.roleID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(
`{"rules":[{"resource":"%s","operation":"namespace.create","access":"allow"},{"resource":"%s","operation":"read","access":"allow"}]}`,
types.ComponentRbacResource(),
"corteza::compose:chart/1/2",
//types.ChartRbacResource(1, 0),
)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestPermissionsDelete(t *testing.T) {
h := newHelper(t)
p := rbac.Global()
// Make sure our user can grant
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
// New role.
permDelRole := h.roleID + 1
h.a.Len(rbac.Global().FindRulesByRoleID(permDelRole), 0)
// Setup a few fake rules for new role
helpers.Grant(rbac.AllowRule(permDelRole, types.ComponentRbacResource(), "namespace.create"))
h.a.Len(p.FindRulesByRoleID(permDelRole), 1)
h.apiInit().
Delete(fmt.Sprintf("/permissions/%d/rules", permDelRole)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
// Make sure all rules for this role are deleted
for _, r := range p.FindRulesByRoleID(permDelRole) {
h.a.True(r.Access == rbac.Inherit)
}
}
func TestPermissionsTrace(t *testing.T) {
h := newHelper(t)
helpers.AllowMe(h, types.ComponentRbacResource(), "grant")
h.apiInit().
Get("/permissions/trace").
Query("roleID[]", "1").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present(`$.response`)).
End()
}

View File

@@ -0,0 +1,150 @@
package compose
import (
"context"
"fmt"
"testing"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
func crissCrossUserRoles(ctx context.Context, s store.Storer, h helper, uu systemTypes.UserSet, rr systemTypes.RoleSet) (map[string]*systemTypes.User, map[string]*systemTypes.Role) {
ux := make(map[string]*systemTypes.User)
for _, r := range uu {
ux[r.Name] = r
}
rx := make(map[string]*systemTypes.Role)
for _, r := range rr {
rx[r.Handle] = r
}
for _, u := range uu {
if _, ok := rx[u.Name]; !ok {
h.a.FailNow(fmt.Sprintf("corresponding role not found for user: r: %s; u: %s", u.Name, u.Handle))
}
rID := rx[u.Name].ID
h.a.NoError(store.CreateRoleMember(ctx, s, &systemTypes.RoleMember{
RoleID: rID,
Resource: fmt.Sprintf("corteza::system:user/%d", u.ID),
}))
u.SetRoles(rID)
}
return ux, rx
}
func Test_record_access_context(t *testing.T) {
t.Skip("skipping due to Envoy not yet refactored")
// ctx, h, s := setup(t)
// loadScenario(ctx, defStore, t, h)
// // setup
// rr, _, err := store.SearchRoles(ctx, s, systemTypes.RoleFilter{})
// h.a.NoError(err)
// uu, _, err := store.SearchUsers(ctx, s, systemTypes.UserFilter{})
// h.a.NoError(err)
// ux, rx := crissCrossUserRoles(ctx, s, h, uu, rr)
// ns, err := store.LookupComposeNamespaceBySlug(ctx, s, "ns1")
// h.a.NoError(err)
// mod, err := store.LookupComposeModuleByNamespaceIDHandle(ctx, s, ns.ID, "mod1")
// h.a.NoError(err)
// records, _, err := store.SearchComposeRecords(ctx, s, mod, types.RecordFilter{})
// h.a.NoError(err)
// rec := records[0]
// for _, r := range rr {
// helpers.Allow(r, types.NamespaceRbacResource(0), "read")
// helpers.Allow(r, types.ModuleRbacResource(0, 0), "read")
// }
// helpers.DenyMe(h, rec.RbacResource(), "read")
// helpers.Allow(rx["owner"], rec.RbacResource(), "read")
// helpers.Allow(rx["creator"], rec.RbacResource(), "read")
// helpers.Allow(rx["updater"], rec.RbacResource(), "read")
// helpers.Allow(rx["deleter"], rec.RbacResource(), "read")
// h.a.NoError(service.UpdateRbacRoles(ctx, testApp.Log, rbac.Global(), nil, nil, nil))
// rbac.Global().Reload(ctx)
// t.Run("generic user with no ctx role", func(t *testing.T) {
// h.apiInit().
// Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)).
// Header("Accept", "application/json").
// Expect(t).
// Status(http.StatusOK).
// Assert(helpers.AssertError("record.errors.notAllowedToRead")).
// End()
// })
// t.Run("user with owner ctx role", func(t *testing.T) {
// h.identityToHelper(ux["owner"])
// h.apiInit().
// Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)).
// Header("Accept", "application/json").
// Expect(t).
// Status(http.StatusOK).
// Assert(helpers.AssertNoErrors).
// End()
// })
// t.Run("user with creator ctx role", func(t *testing.T) {
// h.identityToHelper(ux["creator"])
// h.apiInit().
// Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)).
// Header("Accept", "application/json").
// Expect(t).
// Status(http.StatusOK).
// Assert(helpers.AssertNoErrors).
// End()
// })
// t.Run("user with updater ctx role", func(t *testing.T) {
// h.identityToHelper(ux["updater"])
// h.apiInit().
// Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)).
// Header("Accept", "application/json").
// Expect(t).
// Status(http.StatusOK).
// Assert(helpers.AssertNoErrors).
// End()
// })
// t.Run("user with deleter ctx role", func(t *testing.T) {
// h.identityToHelper(ux["deleter"])
// h.apiInit().
// Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)).
// Header("Accept", "application/json").
// Expect(t).
// Status(http.StatusOK).
// Assert(helpers.AssertNoErrors).
// End()
// })
// t.Run("user with creator ctx role", func(t *testing.T) {
// h.identityToHelper(ux["creator"])
// h.apiInit().
// Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)).
// Header("Accept", "application/json").
// Expect(t).
// Status(http.StatusOK).
// Assert(helpers.AssertNoErrors).
// End()
// })
// t.Run("user with creator ctx role", func(t *testing.T) {
// h.identityToHelper(ux["creator"])
// h.apiInit().
// Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", mod.NamespaceID, mod.ID, rec.ID)).
// Header("Accept", "application/json").
// Expect(t).
// Status(http.StatusOK).
// Assert(helpers.AssertNoErrors).
// End()
// })
}

View File

@@ -0,0 +1,139 @@
package compose
import (
"fmt"
"net/http"
"strconv"
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/steinfletcher/apitest-jsonpath"
)
func TestBatchRecordCreate(t *testing.T) {
h := newHelper(t)
h.clearRecords()
ns := h.makeNamespace("batch testing namespace")
parentModule := h.makeRecordModuleWithFieldsOnNs("record testing module", ns)
childModule := h.makeRecordModuleWithFieldsOnNs("record testing module child", ns,
&types.ModuleField{
Name: "name",
},
&types.ModuleField{
Name: "parent_ref",
Kind: "Record",
Options: types.ModuleFieldOptions{
"moduleID": strconv.FormatUint(parentModule.ID, 10),
},
},
)
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create")
helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update")
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d/record/", parentModule.NamespaceID, parentModule.ID)).
JSON(fmt.Sprintf(`{"values": [], "records": [{"refField": "parent_ref", "set": [{"moduleID": "%d", "values": []}]}]}`, childModule.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present(`$.response.recordID`)).
Assert(jsonpath.Len(`$.response.records`, 1)).
Assert(jsonpath.Present(`$.response.records[0].recordID`)).
End()
}
func TestBatchRecordUpdate(t *testing.T) {
h := newHelper(t)
h.clearRecords()
ns := h.makeNamespace("batch testing namespace")
childModule := h.makeRecordModuleWithFieldsOnNs("record testing module child", ns)
module := h.makeRecordModuleWithFieldsOnNs("record testing module", ns)
helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update")
helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update")
record := h.makeRecord(module)
childRecord := h.makeRecord(childModule, &types.RecordValue{Name: "another_record", Value: strconv.FormatUint(record.ID, 10), Ref: record.ID})
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "Some Name"}], "records": [{"refField": "another_record", "set": [{"moduleID": "%d", "recordID": "%d", "values": [{"name": "name", "value": "Another Name"}]}]}]}`, childModule.ID, childRecord.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.recordID`, strconv.FormatUint(record.ID, 10))).
Assert(jsonpath.Len(`$.response.records`, 1)).
Assert(jsonpath.Equal(`$.response.records[0].recordID`, strconv.FormatUint(childRecord.ID, 10))).
End()
}
func TestBatchRecordDelete(t *testing.T) {
h := newHelper(t)
h.clearRecords()
ns := h.makeNamespace("batch testing namespace")
module := h.makeRecordModuleWithFieldsOnNs("record testing module", ns)
childModule := h.makeRecordModuleWithFieldsOnNs("record testing module child", ns)
helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update", "delete")
record := h.makeRecord(module)
childRecord := h.makeRecord(childModule, &types.RecordValue{Name: "another_record", Value: strconv.FormatUint(record.ID, 10), Ref: record.ID})
h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "Some Name"}], "records": [{"refField": "another_record", "set": [{"deletedAt": "2020-05-15T15:01:02+02:00", "moduleID": "%d", "recordID": "%d", "values": [{"name": "name", "value": "Another Name"}]}]}]}`, childModule.ID, childRecord.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.recordID`, strconv.FormatUint(record.ID, 10))).
Assert(jsonpath.Len(`$.response.records`, 1)).
Assert(jsonpath.Equal(`$.response.records[0].recordID`, strconv.FormatUint(childRecord.ID, 10))).
End()
record = h.lookupRecordByID(module, childRecord.ID)
h.a.NotNil(record.DeletedAt)
}
func TestBatchRecordMixed(t *testing.T) {
h := newHelper(t)
h.clearRecords()
ns := h.makeNamespace("batch testing namespace")
parentModule := h.makeRecordModuleWithFieldsOnNs("record testing module", ns)
childModule := h.makeRecordModuleWithFieldsOnNs("record testing module child", ns,
&types.ModuleField{
Name: "name",
},
&types.ModuleField{
Name: "parent_ref",
Kind: "Record",
Options: types.ModuleFieldOptions{
"moduleID": strconv.FormatUint(parentModule.ID, 10),
},
})
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create")
helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update", "delete")
record := h.makeRecord(parentModule)
childRecord := h.makeRecord(childModule, &types.RecordValue{Name: "parent_ref", Value: strconv.FormatUint(record.ID, 10)})
h.apiInit().
Debug().
Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", parentModule.NamespaceID, parentModule.ID, record.ID)).
Header("Accept", "application/json").
JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "Some Name"}], "records": [{"refField": "parent_ref", "set": [{"moduleID": "%d", "values": [{"name": "name", "value": "Added Name"}]},{"moduleID": "%d", "recordID": "%d", "values": [{"name": "name", "value": "Another Name"}]}]}]}`, childModule.ID, childModule.ID, childRecord.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.recordID`, strconv.FormatUint(record.ID, 10))).
Assert(jsonpath.Len(`$.response.records`, 2)).
Assert(jsonpath.Present(`$.response.records[0].recordID`)).
Assert(jsonpath.Equal(`$.response.records[1].recordID`, strconv.FormatUint(childRecord.ID, 10))).
End()
}

View File

@@ -0,0 +1,210 @@
package compose
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"testing"
"github.com/cortezaproject/corteza/server/compose/dalutils"
"github.com/cortezaproject/corteza/server/compose/rest/request"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/steinfletcher/apitest"
)
func (h helper) apiSendRecordExec(nsID, modID uint64, proc string, args []request.ProcedureArg) *apitest.Response {
payload, err := json.Marshal(request.RecordExec{Args: args})
h.noError(err)
return h.apiInit().
Post(fmt.Sprintf("/namespace/%d/module/%d/record/exec/%s", nsID, modID, proc)).
JSON(string(payload)).
Expect(h.t)
}
func TestRecordExecUnknownProcedure(t *testing.T) {
h := newHelper(t)
h.apiInit().
Post("/namespace/0/module/0/record/exec/test-unexisting-proc").
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("unknown procedure")).
End()
}
func TestRecordExecOrganize(t *testing.T) {
h := newHelper(t)
h.clearRecords()
//helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
//helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read")
helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read", "update")
//helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update")
module := h.repoMakeRecordModuleWithFields(
"record testing module",
&types.ModuleField{Name: "position", Kind: "Number"},
&types.ModuleField{Name: "handle"},
&types.ModuleField{Name: "category"},
)
makeRecord := func(position int, handle, cat string) *types.Record {
return h.makeRecord(module,
&types.RecordValue{Name: "position", Value: strconv.Itoa(position)},
&types.RecordValue{Name: "handle", Value: handle},
&types.RecordValue{Name: "category", Value: cat},
)
}
assertSort := func(expectedHandles, expectedCats string) {
// Using record service for fetching to avoid value pre-fetching etc..
sorting, _ := filter.NewSorting("position ASC")
set, _, err := dalutils.ComposeRecordsList(context.Background(), defDal, module, types.RecordFilter{
ModuleID: module.ID,
NamespaceID: module.NamespaceID,
Sorting: sorting,
})
h.noError(err)
h.a.NotNil(set)
actualHandles := ""
actualCats := ""
_ = set.Walk(func(r *types.Record) error {
//fmt.Printf("%d\t%s\t%s\t%s\n", r.ID,
// r.Values.FilterByName("position")[0].Value,
// r.Values.FilterByName("handle")[0].Value,
// r.Values.FilterByName("category")[0].Value,
//)
v := r.Values.FilterByName("handle")
if len(v) == 1 {
actualHandles += v[0].Value
} else {
actualHandles += strconv.Itoa(len(v))
}
actualCats += r.Values.FilterByName("category")[0].Value[3:]
return nil
})
h.a.Equal(expectedHandles, actualHandles)
h.a.Equal(expectedCats, actualCats)
}
t.Logf("seeding records")
var (
aRec = makeRecord(1, "a", "CAT1")
bRec = makeRecord(2, "b", "CAT1")
cRec = makeRecord(3, "c", "CAT1")
dRec = makeRecord(4, "d", "CAT2")
eRec = makeRecord(5, "e", "CAT2")
fRec = makeRecord(6, "f", "CAT2")
gRec = makeRecord(7, "g", "CAT3")
hRec = makeRecord(8, "h", "CAT3")
iRec = makeRecord(9, "i", "CAT3")
)
// map handle to record ID so we can use it for reordering
rr := map[string]string{
"a": strconv.FormatUint(aRec.ID, 10),
"b": strconv.FormatUint(bRec.ID, 10),
"c": strconv.FormatUint(cRec.ID, 10),
"d": strconv.FormatUint(dRec.ID, 10),
"e": strconv.FormatUint(eRec.ID, 10),
"f": strconv.FormatUint(fRec.ID, 10),
"g": strconv.FormatUint(gRec.ID, 10),
"h": strconv.FormatUint(hRec.ID, 10),
"i": strconv.FormatUint(iRec.ID, 10),
}
t.Logf("testing initial order")
assertSort("abcdefghi", "111222333")
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
t.Logf("moving 'a' to position '6'")
// Move a to the middle
h.apiSendRecordExec(module.NamespaceID, module.ID, "organize", request.ProcedureArgs{
{Name: "recordID", Value: rr["a"]},
{Name: "positionField", Value: "position"},
{Name: "position", Value: "6"}}).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
// abcdefghi
// ^---v
assertSort("bcdeafghi", "112212333")
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
t.Logf("moving 'i' to position '0'")
// Move i to the beginning
h.apiSendRecordExec(module.NamespaceID, module.ID, "organize", request.ProcedureArgs{
{Name: "recordID", Value: rr["i"]},
{Name: "positionField", Value: "position"},
{Name: "position", Value: "0"}}).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
// bcdeafghi
// v<------^
assertSort("ibcdeafgh", "311221233")
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
t.Logf("moving 'b' to position '5'")
// Move b to the 5th place
h.apiSendRecordExec(module.NamespaceID, module.ID, "organize", request.ProcedureArgs{
{Name: "recordID", Value: rr["b"]},
{Name: "filter", Value: "category = 'CAT1'"},
{Name: "positionField", Value: "position"},
{Name: "position", Value: "5"}}).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
// ibcdeafgh
// ^->v
assertSort("icdebafgh", "312211233")
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
t.Logf("moving 'b' to category CAT2")
// This will keep order of letters but move b to category-2
h.apiSendRecordExec(module.NamespaceID, module.ID, "organize", request.ProcedureArgs{
{Name: "recordID", Value: rr["b"]},
{Name: "groupField", Value: "category"},
{Name: "group", Value: "CAT2"}}).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
// icdebfagh
// ^
assertSort("icdebafgh", "312221233")
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
// ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
lRec := h.lookupRecordByID(module, bRec.ID)
h.a.NotNil(lRec.Values)
h.a.Len(lRec.Values.FilterByName("category"), 1)
h.a.Equal("CAT2", lRec.Values.FilterByName("category")[0].Value)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
namespaces:
ns1:
name: ns1 name

View File

@@ -0,0 +1,69 @@
# Provides a complete set of things to cover regarding modules and module fields.
# mod1: defines more complicated fields, relates to mod2
# mod2: defines a self-ref, relates to mod3
# mod3: just a placeholder
namespace: ns1
modules:
mod1:
name: mod1 name
fields:
f1:
label: f1 label
kind: String
required: true
f2:
label: f2 label
kind: Select
options:
options:
- f2 opt 1
- f2 opt 2
- f2 opt 3
f3:
label: f3 label
kind: Select
options:
options:
- ☆☆☆☆☆
- ★☆☆☆☆
f4:
label: f4 label
kind: Record
options:
labelField: f_label
module: mod2
queryFields:
- f1
mod2:
name: mod2 name
fields:
f_label:
label: f_label record label
f_ref_self:
label: f_ref_self label
kind: Record
options:
labelField: f_label
module: mod2
queryFields:
- f1
f1:
label: f1 label
kind: String
required: true
f2:
label: f2 label
kind: Record
options:
labelField: f_label
module: mod3
queryFields:
- f1
mod3:
name: mod3 name
fields:
f_label:
label: f_label record label

View File

@@ -0,0 +1,47 @@
namespace: ns1
pages:
pg1:
title: pg1 title
blocks:
- title: pg1 b1
kind: Content
xywh: [0, 0, 1, 1]
options:
body: pg1 b1 content body
- title: pg1 b2
kind: RecordList
xywh: [0, 1, 1, 1]
options:
module: mod1
fields:
- name: f1
- name: f2
variants:
bodyBg: white
border: primary
headerBg: white
headerText: primary
- title: pg1 b3
kind: Chart
xywh: [0, 2, 1, 1]
options:
chart: chr1
variants:
bodyBg: white
border: primary
headerBg: white
headerText: primary
rpg2:
handle: rpg2
module: mod1
title: Record page for module "mod1"
blocks:
- title: rpg2 b1 title
kind: Record
options:
fields:
- name: f1
- name: f2

View File

@@ -0,0 +1,18 @@
namespace: ns1
charts:
chr1:
name: chr1 name
config:
reports:
- dimensions:
- field: Status
modifier: (no grouping / buckets)
filter: ""
metrics:
- backgroundColor: '#11ff57'
field: count
fixTooltips: true
type: pie
module: mod1
renderer: {}
colorScheme: tableau.Tableau10

View File

@@ -0,0 +1,3 @@
namespaces:
ns1:
name: ns1 name

View File

@@ -0,0 +1,17 @@
namespace: ns1
modules:
mod1:
name: mod1 name
fields:
f1:
label: f1 label
kind: String
required: true
f2:
label: f2 label
kind: Select
options:
options:
- f2 opt 1
- f2 opt 2
- f2 opt 3

View File

@@ -0,0 +1,60 @@
namespace: ns1
pages:
pg1:
title: pg1 title
blocks:
- title: pg1 b1
kind: Content
xywh: [0, 0, 1, 1]
options:
body: pg1 b1 content body
- title: pg1 b2
kind: RecordList
xywh: [0, 1, 1, 1]
options:
module: mod1
fields:
- name: f1
- name: f2
- title: pg1 b3 automation 1
kind: Automation
xywh: [0, 0, 0, 0]
options:
buttons:
- label: workflow button
resourceType: compose:record
workflow: wf1
stepID: 4
- title: pg1 b3 automation 2
kind: Automation
xywh: [0, 0, 0, 0]
options:
buttons:
- label: script button
script: /client-scripts/compose/test.js:default
- title: pg1 b3 automation 3
kind: Automation
xywh: [0, 0, 0, 0]
options:
buttons:
- label: placeholder button
- title: pg1 b3 automation 4
kind: Automation
xywh: [0, 0, 0, 0]
rpg2:
handle: rpg2
module: mod1
title: Record page for module "mod1"
blocks:
- title: rpg2 b1 title
kind: Record
options:
fields:
- name: f1
- name: f2

View File

@@ -0,0 +1,36 @@
workflows:
wf1:
meta:
name: wf1
enabled: true
keepSessions: 0
triggers:
- resourceType: compose:record
eventType: beforeCreate
constraints:
- name: namespace.handle
op: =
values:
- ns1
enabled: true
stepID: 4
steps:
- id: 4
kind: prompt
ref: alert
arguments:
- target: message
source: ""
expr: ""
value: Test
type: String
tests: []
results: []
meta:
name: ""
description: ""
visual:
id: "4"
parent: "1"
value: null
xywh: [0, 0, 0, 0]

View File

@@ -0,0 +1,3 @@
namespaces:
ns1:
name: ns1 name

View File

@@ -0,0 +1,3 @@
namespaces:
ns1:
name: ns1 name

View File

@@ -0,0 +1,42 @@
namespace: ns1
modules:
mod1:
name: mod1 name
fields:
f1:
label: f1 label
kind: String
required: true
f2:
label: f2 label
kind: Select
options:
options:
- f2 opt 1
- f2 opt 2
- f2 opt 3
f3:
label: f3 label
kind: Record
options:
labelField: f_label
module: mod2
queryFields:
- f1
mod2:
name: mod2 name
fields:
f_label:
label: f_label record label
f1:
label: f1 label
kind: String
required: true
mod3:
name: mod3 name
fields:
f1:
label: f1 label
kind: String

View File

@@ -0,0 +1,47 @@
namespace: ns1
pages:
pg1:
title: pg1 title
blocks:
- title: pg1 b1
kind: Content
xywh: [0, 0, 1, 1]
options:
body: pg1 b1 content body
- title: pg1 b2
kind: RecordList
xywh: [0, 1, 1, 1]
options:
module: mod1
fields:
- name: f1
- name: f2
- title: pg1 b3
kind: Chart
xywh: [0, 2, 1, 1]
options:
chart: chr1
rpg2:
handle: rpg2
module: mod1
title: Record page for module "mod1"
blocks:
- title: rpg2 b1 title
kind: Record
options:
fields:
- name: f1
- name: f2
pages:
pg2:
title: pg2 title
blocks:
- title: pg2 b1
kind: Content
xywh: [0, 0, 1, 1]
options:
body: pg2 b1 content body

View File

@@ -0,0 +1,18 @@
namespace: ns1
charts:
chr1:
name: chr1 name
config:
reports:
- dimensions:
- field: Status
modifier: (no grouping / buckets)
filter: ""
metrics:
- backgroundColor: '#11ff57'
field: count
fixTooltips: true
type: pie
module: mod1
renderer: {}
colorScheme: tableau.Tableau10

View File

@@ -0,0 +1,3 @@
namespaces:
ns1:
name: ns1 name

View File

@@ -0,0 +1,42 @@
namespace: ns1
modules:
mod1:
name: mod1 name
fields:
f1:
label: f1 label
kind: String
required: true
f2:
label: f2 label
kind: Select
options:
options:
- f2 opt 1
- f2 opt 2
- f2 opt 3
f3:
label: f3 label
kind: Record
options:
labelField: f_label
module: mod2
queryFields:
- f1
mod2:
name: mod2 name
fields:
f_label:
label: f_label record label
f1:
label: f1 label
kind: String
required: true
mod3:
name: mod3 name
fields:
f1:
label: f1 label
kind: String

View File

@@ -0,0 +1,47 @@
namespace: ns1
pages:
pg1:
title: pg1 title
blocks:
- title: pg1 b1
kind: Content
xywh: [0, 0, 1, 1]
options:
body: pg1 b1 content body
- title: pg1 b2
kind: RecordList
xywh: [0, 1, 1, 1]
options:
module: mod1
fields:
- name: f1
- name: f2
- title: pg1 b3
kind: Chart
xywh: [0, 2, 1, 1]
options:
chart: chr1
rpg2:
handle: rpg2
module: mod1
title: Record page for module "mod1"
blocks:
- title: rpg2 b1 title
kind: Record
options:
fields:
- name: f1
- name: f2
pages:
pg2:
title: pg2 title
blocks:
- title: pg2 b1
kind: Content
xywh: [0, 0, 1, 1]
options:
body: pg2 b1 content body

View File

@@ -0,0 +1,18 @@
namespace: ns1
charts:
chr1:
name: chr1 name
config:
reports:
- dimensions:
- field: Status
modifier: (no grouping / buckets)
filter: ""
metrics:
- backgroundColor: '#11ff57'
field: count
fixTooltips: true
type: pie
module: mod1
renderer: {}
colorScheme: tableau.Tableau10

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,30 @@
namespaces:
ns1:
name: ns1 name
modules:
mod1:
name: mod1 name
fields:
f1:
label: f1 label
kind: String
required: true
f2:
label: f2 label
kind: Select
options:
options:
- f2 opt 1
- f2 opt 2
- f2 opt 3
records:
mod1:
- createdBy: user_createdby
updatedBy: user_updatedby
deletedBy: user_deletedby
ownedBy: user_ownedby
values:
f1: "mod1 f1 v1"
f2: "f2 opt 1"

View File

@@ -0,0 +1,49 @@
users:
user_createdby:
email: user_createdby@test.tld
name: creator
user_updatedby:
email: user_updatedby@test.tld
name: updater
user_deletedby:
email: user_deletedby@test.tld
name: deleter
user_ownedby:
email: user_ownedby@test.tld
name: owner
roles:
owner:
name: Owner
meta:
context:
expr: resource.ownedBy == userID
resourceType:
- corteza::compose:record
creator:
name: Creator
meta:
context:
expr: resource.createdBy == userID
resourceType:
- corteza::compose:record
updater:
name: Updater
meta:
context:
expr: resource.updatedBy == userID
resourceType:
- corteza::compose:record
deleter:
name: Deleter
meta:
context:
expr: resource.deletedBy == userID
resourceType:
- corteza::compose:record

View File

@@ -0,0 +1,6 @@
John,Doe,120 jefferson st.,Riverside, NJ, 08075
Jack,McGinnis,220 hobo Av.,Phila, PA,09119
"John ""Da Man""",Repici,120 Jefferson St.,Riverside, NJ,08075
Stephen,Tyler,"7452 Terrace ""At the Plaza"" road",SomeTown,SD, 91234
,Blankman,,SomeTown, SD, 00298
"Joan ""the bone"", Anne",Jet,"9th, at Terrace plc",Desert City,CO,00123
1 John Doe 120 jefferson st. Riverside NJ 08075
2 Jack McGinnis 220 hobo Av. Phila PA 09119
3 John "Da Man" Repici 120 Jefferson St. Riverside NJ 08075
4 Stephen Tyler 7452 Terrace "At the Plaza" road SomeTown SD 91234
5 Blankman SomeTown SD 00298
6 Joan "the bone", Anne Jet 9th, at Terrace plc Desert City CO 00123

BIN
server/tests/compose/testdata/test.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,72 @@
package dal
import (
"context"
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
)
func Test_dal_codec_alias(t *testing.T) {
t.Skip("needs refactoring")
model := &dal.Model{
Ident: "compose_record_partitioned",
Attributes: dal.AttributeSet{
&dal.Attribute{Ident: "ID", Type: &dal.TypeID{}, Store: &dal.CodecAlias{Ident: "id"}, PrimaryKey: true},
&dal.Attribute{Ident: "intr_vID", Type: &dal.TypeID{}, Store: &dal.CodecAlias{Ident: "vID"}},
&dal.Attribute{Ident: "intr_vRef", Type: &dal.TypeRef{}, Store: &dal.CodecAlias{Ident: "vRef"}},
&dal.Attribute{Ident: "intr_vTimestamp", Type: &dal.TypeTimestamp{}, Store: &dal.CodecAlias{Ident: "vTimestamp"}},
&dal.Attribute{Ident: "intr_vTime", Type: &dal.TypeTime{}, Store: &dal.CodecAlias{Ident: "vTime"}},
&dal.Attribute{Ident: "intr_vDate", Type: &dal.TypeDate{}, Store: &dal.CodecAlias{Ident: "vDate"}},
&dal.Attribute{Ident: "intr_vNumber", Type: &dal.TypeNumber{}, Store: &dal.CodecAlias{Ident: "vNumber"}},
&dal.Attribute{Ident: "intr_vText", Type: &dal.TypeText{}, Store: &dal.CodecAlias{Ident: "vText"}},
&dal.Attribute{Ident: "intr_vBoolean_T", Type: &dal.TypeBoolean{}, Store: &dal.CodecAlias{Ident: "vBoolean_T"}},
&dal.Attribute{Ident: "intr_vBoolean_F", Type: &dal.TypeBoolean{}, Store: &dal.CodecAlias{Ident: "vBoolean_F"}},
&dal.Attribute{Ident: "intr_vEnum", Type: &dal.TypeEnum{}, Store: &dal.CodecAlias{Ident: "vEnum"}},
&dal.Attribute{Ident: "intr_vGeometry", Type: &dal.TypeGeometry{}, Store: &dal.CodecAlias{Ident: "vGeometry"}},
&dal.Attribute{Ident: "intr_vJSON", Type: &dal.TypeJSON{}, Store: &dal.CodecAlias{Ident: "vJSON"}},
&dal.Attribute{Ident: "intr_vBlob", Type: &dal.TypeBlob{}, Store: &dal.CodecAlias{Ident: "vBlob"}},
&dal.Attribute{Ident: "intr_vUUID", Type: &dal.TypeUUID{}, Store: &dal.CodecAlias{Ident: "vUUID"}},
},
}
var (
rIn = types.Record{ID: 42, CreatedAt: time.Now()}
rOut = types.Record{}
)
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vID", Value: "34324"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vRef", Value: "32423"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vTimestamp", Value: "2022-01-01T10:10:10"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vTime", Value: "04:10:10"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vDate", Value: "2022-01-01"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vNumber", Value: "2423423"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vText", Value: "lorm ipsum "})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vBoolean_T", Value: "1"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vBoolean_F", Value: ""})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vEnum", Value: "abc"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vGeometry", Value: `{"lat":1,"lng":1}`})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vJSON", Value: `[{"bool":true"}]`})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vBlob", Value: "0110101"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "intr_vUUID", Value: "ba485865-54f9-44de-bde8-6965556c022a"})
bootstrap(t, func(ctx context.Context, t *testing.T, h helper, svc dal.FullService) {
h.cleanupDal()
_, err := svc.ReplaceModel(ctx, nil, model)
h.a.NoError(err)
h.a.NoError(svc.Create(ctx, model.ToFilter(), dal.CreateOperations(), &rIn))
h.a.NoError(svc.Lookup(ctx, model.ToFilter(), dal.LookupOperations(), dal.PKValues{"id": rIn.ID}, &rOut))
for _, inVal := range rIn.Values {
outVal := rOut.Values.Get(inVal.Name, 0)
h.a.Equal(inVal.Value, outVal.Value, "field", inVal.Name)
}
})
}

View File

@@ -0,0 +1,68 @@
package dal
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
)
func Test_dal_codec_json(t *testing.T) {
t.Skip("needs refactoring")
model := &dal.Model{
Ident: "compose_record",
Attributes: dal.AttributeSet{
&dal.Attribute{Ident: "ID", Type: &dal.TypeID{}, Store: &dal.CodecAlias{Ident: "id"}, PrimaryKey: true},
&dal.Attribute{Ident: "vID", Type: &dal.TypeID{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vRef", Type: &dal.TypeRef{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vTimestamp", Type: &dal.TypeTimestamp{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vTime", Type: &dal.TypeTime{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vDate", Type: &dal.TypeDate{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vNumber", Type: &dal.TypeNumber{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vText", Type: &dal.TypeText{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vBoolean_T", Type: &dal.TypeBoolean{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vBoolean_F", Type: &dal.TypeBoolean{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vEnum", Type: &dal.TypeEnum{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vGeometry", Type: &dal.TypeGeometry{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vJSON", Type: &dal.TypeJSON{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vBlob", Type: &dal.TypeBlob{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
&dal.Attribute{Ident: "vUUID", Type: &dal.TypeUUID{}, Store: &dal.CodecRecordValueSetJSON{Ident: "values"}},
},
}
var (
rIn = types.Record{ID: 42}
rOut = types.Record{}
)
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vID", Value: "34324"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vRef", Value: "32423"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vTimestamp", Value: "2022-01-01T10:10:10"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vTime", Value: "04:10:10"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vDate", Value: "2022-01-01"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vNumber", Value: "2423423"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vText", Value: "lorm ipsum "})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBoolean_T", Value: "1"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBoolean_F", Value: ""})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vEnum", Value: "abc"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vGeometry", Value: `{"lat":1,"lng":1}`})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vJSON", Value: `[{"bool":true"}]`})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBlob", Value: "0110101"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vUUID", Value: "ba485865-54f9-44de-bde8-6965556c022a"})
bootstrap(t, func(ctx context.Context, t *testing.T, h helper, svc dal.FullService) {
_, err := svc.ReplaceModel(ctx, nil, model)
h.a.NoError(err)
h.a.NoError(svc.Create(ctx, model.ToFilter(), dal.CreateOperations(), &rIn))
h.a.NoError(svc.Lookup(ctx, model.ToFilter(), dal.LookupOperations(), dal.PKValues{"id": rIn.ID}, &rOut))
for _, inVal := range rIn.Values {
outVal := rOut.Values.Get(inVal.Name, 0)
h.a.Equal(inVal.Value, outVal.Value, "field", inVal.Name)
}
})
}

View File

@@ -0,0 +1,141 @@
package dal
import (
"context"
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
)
func Test_dal_codec_plain(t *testing.T) {
t.Skip("needs refactoring")
model := &dal.Model{
Ident: "compose_record_partitioned",
Attributes: dal.AttributeSet{
&dal.Attribute{Ident: "ID", Type: &dal.TypeID{}, Store: &dal.CodecAlias{Ident: "id"}, PrimaryKey: true},
&dal.Attribute{Ident: "vID", Type: &dal.TypeID{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vRef", Type: &dal.TypeRef{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vTimestamp", Type: &dal.TypeTimestamp{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vTime", Type: &dal.TypeTime{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vDate", Type: &dal.TypeDate{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vNumber", Type: &dal.TypeNumber{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vText", Type: &dal.TypeText{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vBoolean_T", Type: &dal.TypeBoolean{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vBoolean_F", Type: &dal.TypeBoolean{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vEnum", Type: &dal.TypeEnum{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vGeometry", Type: &dal.TypeGeometry{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vJSON", Type: &dal.TypeJSON{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vBlob", Type: &dal.TypeBlob{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vUUID", Type: &dal.TypeUUID{}, Store: &dal.CodecPlain{}},
},
}
var (
rIn = types.Record{ID: 42}
rOut = types.Record{}
)
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vID", Value: "34324"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vRef", Value: "32423"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vTimestamp", Value: "2022-01-01T10:10:10"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vTime", Value: "04:10:10"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vDate", Value: "2022-01-01"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vNumber", Value: "2423423"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vText", Value: "lorm ipsum "})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBoolean_T", Value: "1"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBoolean_F", Value: ""})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vEnum", Value: "abc"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vGeometry", Value: `{"lat":1,"lng":1}`})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vJSON", Value: `[{"bool":true"}]`})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBlob", Value: "0110101"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vUUID", Value: "ba485865-54f9-44de-bde8-6965556c022a"})
bootstrap(t, func(ctx context.Context, t *testing.T, h helper, svc dal.FullService) {
_, err := svc.ReplaceModel(ctx, nil, model)
h.a.NoError(err)
h.a.NoError(svc.Create(ctx, model.ToFilter(), dal.CreateOperations(), &rIn))
h.a.NoError(svc.Lookup(ctx, model.ToFilter(), dal.LookupOperations(), dal.PKValues{"id": rIn.ID}, &rOut))
for _, inVal := range rIn.Values {
outVal := rOut.Values.Get(inVal.Name, 0)
h.a.Equal(inVal.Value, outVal.Value, "field", inVal.Name)
}
})
}
func benchmark_dal_codec_plain(b *testing.B, count int) {
b.Skip("needs refactoring")
model := &dal.Model{
Ident: "compose_record_partitioned",
Attributes: dal.AttributeSet{
&dal.Attribute{Ident: "ID", Type: &dal.TypeID{}, Store: &dal.CodecAlias{Ident: "id"}, PrimaryKey: true},
&dal.Attribute{Ident: "vID", Type: &dal.TypeID{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vRef", Type: &dal.TypeRef{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vTimestamp", Type: &dal.TypeTimestamp{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vTime", Type: &dal.TypeTime{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vDate", Type: &dal.TypeDate{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vNumber", Type: &dal.TypeNumber{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vText", Type: &dal.TypeText{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vBoolean_T", Type: &dal.TypeBoolean{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vBoolean_F", Type: &dal.TypeBoolean{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vEnum", Type: &dal.TypeEnum{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vGeometry", Type: &dal.TypeGeometry{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vJSON", Type: &dal.TypeJSON{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vBlob", Type: &dal.TypeBlob{}, Store: &dal.CodecPlain{}},
&dal.Attribute{Ident: "vUUID", Type: &dal.TypeUUID{}, Store: &dal.CodecPlain{}},
},
}
var (
rIn = types.Record{ID: 42}
)
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vID", Value: "34324"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vRef", Value: "32423"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vTimestamp", Value: "2022-01-01T10:10:10"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vTime", Value: "04:10:10"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vDate", Value: "2022-01-01"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vNumber", Value: "2423423"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vText", Value: "lorm ipsum "})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBoolean_T", Value: "true"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBoolean_F", Value: "false"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vEnum", Value: "abc"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vGeometry", Value: `{"lat":1,"lng":1}`})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vJSON", Value: `[{"bool":true"}]`})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vBlob", Value: "0110101"})
rIn.Values = rIn.Values.Set(&types.RecordValue{Name: "vUUID", Value: "ba485865-54f9-44de-bde8-6965556c022a"})
bootstrapBenchmark(b, func(ctx context.Context, b *testing.B, h helper, svc dal.FullService) {
_, err := svc.ReplaceModel(ctx, nil, model)
h.a.NoError(err)
ctr := uint64(1)
for rep := 0; rep < b.N; rep++ {
insert := make([]dal.ValueGetter, 0, 100)
for i := 0; i < 100; i++ {
tmp := rIn
tmp.ID = ctr
ctr++
insert = append(insert, &tmp)
}
b.StartTimer()
h.a.NoError(svc.Create(ctx, model.ToFilter(), dal.CreateOperations(), insert...))
b.StopTimer()
}
})
}
func Benchmark_dal_codec_plain_1(b *testing.B) { benchmark_dal_codec_plain(b, 1) }
func Benchmark_dal_codec_plain_10(b *testing.B) { benchmark_dal_codec_plain(b, 10) }
func Benchmark_dal_codec_plain_100(b *testing.B) { benchmark_dal_codec_plain(b, 100) }
func Benchmark_dal_codec_plain_1000(b *testing.B) { benchmark_dal_codec_plain(b, 1000) }
func Benchmark_dal_codec_plain_10000(b *testing.B) { benchmark_dal_codec_plain(b, 10000) }

View File

@@ -0,0 +1,193 @@
package dal
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
)
func Test_dal_crud_compose_record_create(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
ctx := h.secCtx()
helpers.AllowMeModuleCRUD(h)
helpers.AllowMeRecordCRUD(h)
ns := h.createNamespace("test")
module := createModuleFromGenerics(ctx, t, "ok_module.json", ns.ID, &types.ModuleConfig{
DAL: types.ModuleConfigDAL{
ConnectionID: 0,
},
})
// create
h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d/record/", ns.ID, module.ID)).
Body(loadRequestFromGenerics(t, "ok_record.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
rsp := h.apiInit().
Get(fmt.Sprintf("/compose/namespace/%d/module/%d/record/", ns.ID, module.ID)).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
aux := composeRecordSearchRestRsp{}
dd := json.NewDecoder(rsp.Response.Body)
h.a.NoError(dd.Decode(&aux))
r := aux.Response.Set[0]
h.a.NotEqual(uint64(0), r.ID)
h.a.Equal(module.ID, r.ModuleID)
h.a.Equal(module.NamespaceID, r.NamespaceID)
h.a.Equal(h.cUser.ID, r.OwnedBy)
h.a.NotEqual(time.Time{}, r.CreatedAt)
h.a.Equal(h.cUser.ID, r.CreatedBy)
h.a.Nil(r.UpdatedAt)
h.a.Equal(uint64(0), r.UpdatedBy)
h.a.Nil(r.DeletedAt)
h.a.Equal(uint64(0), r.DeletedBy)
h.a.Equal("me@test.tld", r.Values.Get("email", 0).Value)
h.a.Equal("Me", r.Values.Get("name", 0).Value)
h.a.Equal("42", r.Values.Get("a_number", 0).Value)
}
func Test_dal_crud_compose_record_update(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
ctx := h.secCtx()
helpers.AllowMeModuleCRUD(h)
helpers.AllowMeRecordCRUD(h)
ns := h.createNamespace("test")
module := createModuleFromGenerics(ctx, t, "ok_module.json", ns.ID, &types.ModuleConfig{
DAL: types.ModuleConfigDAL{
ConnectionID: 0,
},
})
record := createRecordFromGenerics(ctx, t, "ok_record.json", ns.ID, module.ID)
h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d/record/%d", ns.ID, module.ID, record.ID)).
Body(loadRequestFromGenerics(t, "ok_record_update.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
rsp := h.apiInit().
Get(fmt.Sprintf("/compose/namespace/%d/module/%d/record/", ns.ID, module.ID)).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
aux := composeRecordSearchRestRsp{}
dd := json.NewDecoder(rsp.Response.Body)
h.a.NoError(dd.Decode(&aux))
r := aux.Response.Set[0]
h.a.NotEqual(uint64(0), r.ID)
h.a.Equal(module.ID, r.ModuleID)
h.a.Equal(module.NamespaceID, r.NamespaceID)
h.a.Equal(h.cUser.ID, r.OwnedBy)
h.a.NotEqual(time.Time{}, r.CreatedAt)
h.a.Equal(h.cUser.ID, r.CreatedBy)
h.a.NotNil(r.UpdatedAt)
h.a.Equal(h.cUser.ID, r.UpdatedBy)
h.a.Nil(r.DeletedAt)
h.a.Equal(uint64(0), r.DeletedBy)
h.a.Equal("me+updated@test.tld", r.Values.Get("email", 0).Value)
h.a.Equal("Me updated", r.Values.Get("name", 0).Value)
h.a.Equal("43", r.Values.Get("a_number", 0).Value)
}
func Test_dal_crud_compose_record_delete(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
ctx := h.secCtx()
helpers.AllowMeModuleCRUD(h)
helpers.AllowMeRecordCRUD(h)
ns := h.createNamespace("test")
module := createModuleFromGenerics(ctx, t, "ok_module.json", ns.ID, &types.ModuleConfig{
DAL: types.ModuleConfigDAL{
ConnectionID: 0,
},
})
record := createRecordFromGenerics(ctx, t, "ok_record.json", ns.ID, module.ID)
h.apiInit().
Delete(fmt.Sprintf("/compose/namespace/%d/module/%d/record/%d", ns.ID, module.ID, record.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
rsp := h.apiInit().
Get(fmt.Sprintf("/compose/namespace/%d/module/%d/record/%d", ns.ID, module.ID, record.ID)).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
aux := composeRecordRestRsp{}
dd := json.NewDecoder(rsp.Response.Body)
h.a.NoError(dd.Decode(&aux))
r := aux.Response
h.a.NotEqual(uint64(0), r.ID)
h.a.Equal(module.ID, r.ModuleID)
h.a.Equal(module.NamespaceID, r.NamespaceID)
h.a.Equal(h.cUser.ID, r.OwnedBy)
h.a.NotEqual(time.Time{}, r.CreatedAt)
h.a.Equal(h.cUser.ID, r.CreatedBy)
h.a.Nil(r.UpdatedAt)
h.a.Equal(uint64(0), r.UpdatedBy)
h.a.NotNil(r.DeletedAt)
h.a.Equal(h.cUser.ID, r.DeletedBy)
}

View File

@@ -0,0 +1,189 @@
package dal
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
)
func Test_dal_crud_issues_compose_module_missing_sensitivity(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
helpers.AllowMeModuleCRUD(h)
ns := h.createNamespace("test")
aux := &composeModuleRestRsp{}
rsp := h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/", ns.ID)).
Body(loadRequestFromGenerics(t, "nok_module_missing_sensitivity_level.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.moduleID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
dd := json.NewDecoder(rsp.Response.Body)
h.a.NoError(dd.Decode(&aux))
rsp = h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d", ns.ID, aux.Response.ID)).
Body(loadRequestFromGenerics(t, "ok_module.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.moduleID")).
Assert(jsonpath.NotPresent("$.response.issues")).
End()
rsp = h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d", ns.ID, aux.Response.ID)).
Body(loadRequestFromGenerics(t, "nok_module_missing_sensitivity_level.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.moduleID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
rsp = h.apiInit().
Delete(fmt.Sprintf("/compose/namespace/%d/module/%d", ns.ID, aux.Response.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.success.message")).
End()
}
func Test_dal_crud_issues_compose_module_field_missing_sensitivity(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
helpers.AllowMeModuleCRUD(h)
ns := h.createNamespace("test")
aux := &composeModuleRestRsp{}
rsp := h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/", ns.ID)).
Body(loadRequestFromGenerics(t, "nok_module_missing_field_sensitivity_level.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.moduleID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
dd := json.NewDecoder(rsp.Response.Body)
h.a.NoError(dd.Decode(&aux))
rsp = h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d", ns.ID, aux.Response.ID)).
Body(loadRequestFromGenerics(t, "ok_module.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.moduleID")).
Assert(jsonpath.NotPresent("$.response.issues")).
End()
rsp = h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d", ns.ID, aux.Response.ID)).
Body(loadRequestFromGenerics(t, "nok_module_missing_field_sensitivity_level.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.moduleID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
rsp = h.apiInit().
Delete(fmt.Sprintf("/compose/namespace/%d/module/%d", ns.ID, aux.Response.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.success.message")).
End()
}
func Test_dal_crud_issues_compose_module_nok_connection(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
helpers.AllowMeModuleCRUD(h)
helpers.AllowMeDalConnectionCRUD(h)
ns := h.createNamespace("test")
conn := createConnectionFromGenerics(h.secCtx(), t, "nok_connection_connectivity.json")
aux := &composeModuleRestRsp{}
rsp := h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/", ns.ID)).
Body(loadRequestFromScenarioWithConnection(t, "module.json", conn.ID)).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.moduleID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
dd := json.NewDecoder(rsp.Response.Body)
h.a.NoError(dd.Decode(&aux))
h.apiInit().
Put(fmt.Sprintf("/system/dal/connections/%d", conn.ID)).
Body(loadRequestFromGenerics(t, "ok_connection.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.connectionID")).
Assert(jsonpath.NotPresent("$.response.issues")).
End()
rsp = h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d", ns.ID, aux.Response.ID)).
Body(loadRequestFromScenarioWithConnection(t, "module.json", conn.ID)).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.moduleID")).
Assert(jsonpath.NotPresent("$.response.issues")).
End()
rsp = h.apiInit().
Delete(fmt.Sprintf("/compose/namespace/%d/module/%d", ns.ID, aux.Response.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}

View File

@@ -0,0 +1,100 @@
package dal
import (
"fmt"
"net/http"
"testing"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
)
func Test_dal_crud_issues_compose_record_nok_connection(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
helpers.AllowMeDalConnectionCRUD(h)
helpers.AllowMeModuleCRUD(h)
helpers.AllowMeRecordCRUD(h)
ns := h.createNamespace("test")
connection := createConnectionFromGenerics(h.secCtx(), t, "nok_connection_connectivity.json")
module := createModuleFromGenerics(h.secCtx(), t, "ok_module.json", ns.ID, &types.ModuleConfig{
DAL: types.ModuleConfigDAL{
ConnectionID: connection.ID,
},
})
h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d/record/", ns.ID, module.ID)).
Body(loadRequestFromGenerics(t, "ok_record.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertErrorP(fmt.Sprintf("connection %d has issues", connection.ID))).
End()
}
func Test_dal_crud_issues_compose_record_nok_model(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
helpers.AllowMeDalConnectionCRUD(h)
helpers.AllowMeModuleCRUD(h)
helpers.AllowMeRecordCRUD(h)
ns := h.createNamespace("test")
connection := createConnectionFromGenerics(h.secCtx(), t, "ok_connection.json")
module := createModuleFromGenerics(h.secCtx(), t, "nok_module_sensitivity_level.json", ns.ID, &types.ModuleConfig{
DAL: types.ModuleConfigDAL{
ConnectionID: connection.ID,
},
})
h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d/record/", ns.ID, module.ID)).
Body(loadRequestFromGenerics(t, "ok_record.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertErrorP(fmt.Sprintf("model %d has issues", module.ID))).
End()
}
func Test_dal_crud_issues_compose_record_ok(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
helpers.AllowMeDalConnectionCRUD(h)
helpers.AllowMeModuleCRUD(h)
helpers.AllowMeRecordCRUD(h)
ns := h.createNamespace("test")
connection := createConnectionFromGenerics(h.secCtx(), t, "ok_connection.json")
module := createModuleFromGenerics(h.secCtx(), t, "ok_module.json", ns.ID, &types.ModuleConfig{
DAL: types.ModuleConfigDAL{
ConnectionID: connection.ID,
},
})
h.apiInit().
Post(fmt.Sprintf("/compose/namespace/%d/module/%d/record/", ns.ID, module.ID)).
Body(loadRequestFromGenerics(t, "ok_record.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}

View File

@@ -0,0 +1,129 @@
package dal
import (
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/cortezaproject/corteza/server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
)
func Test_dal_crud_issues_connection_connectivity(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
helpers.AllowMeDalConnectionCRUD(h)
aux := &dalConnectionRestRsp{}
rsp := h.apiInit().
Post("/system/dal/connections/").
Body(loadRequestFromGenerics(t, "nok_connection_connectivity.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.connectionID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
dd := json.NewDecoder(rsp.Response.Body)
h.a.NoError(dd.Decode(&aux))
h.apiInit().
Put(fmt.Sprintf("/system/dal/connections/%d", aux.Response.ID)).
Body(loadRequestFromGenerics(t, "ok_connection.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.connectionID")).
Assert(jsonpath.NotPresent("$.response.issues")).
End()
h.apiInit().
Put(fmt.Sprintf("/system/dal/connections/%d", aux.Response.ID)).
Body(loadRequestFromGenerics(t, "nok_connection_connectivity.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.connectionID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
h.apiInit().
Delete(fmt.Sprintf("/system/dal/connections/%d", aux.Response.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.success.message")).
End()
}
func Test_dal_crud_issues_connection_sensitivity(t *testing.T) {
t.Skip("needs refactoring")
h := newHelperT(t)
defer h.cleanupDal()
helpers.AllowMeDalConnectionCRUD(h)
aux := &dalConnectionRestRsp{}
rsp := h.apiInit().
Post("/system/dal/connections/").
Body(loadRequestFromGenerics(t, "nok_connection_sensitivity.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.connectionID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
dd := json.NewDecoder(rsp.Response.Body)
h.a.NoError(dd.Decode(&aux))
h.apiInit().
Put(fmt.Sprintf("/system/dal/connections/%d", aux.Response.ID)).
Body(loadRequestFromGenerics(t, "ok_connection.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.connectionID")).
Assert(jsonpath.NotPresent("$.response.issues")).
End()
h.apiInit().
Put(fmt.Sprintf("/system/dal/connections/%d", aux.Response.ID)).
Body(loadRequestFromGenerics(t, "nok_connection_sensitivity.json")).
Header("Accept", "application/json").
Header("Content-Type", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.response.connectionID")).
Assert(jsonpath.Len("$.response.issues", 1)).
End()
h.apiInit().
Delete(fmt.Sprintf("/system/dal/connections/%d", aux.Response.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Present("$.success.message")).
End()
}

View File

@@ -0,0 +1,359 @@
package dal
import (
"context"
"fmt"
"os"
"testing"
composeService "github.com/cortezaproject/corteza/server/compose/service"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/cortezaproject/corteza/server/tests/dal/setup/mysql"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
type (
driver struct {
name string
dsn string
setup func(ctx context.Context, t wrapTest, dsn string)
}
dalConnectionRestRsp struct {
Response *types.DalConnection
}
composeModuleRestRsp struct {
Response *composeTypes.Module
}
composeRecordRestRsp struct {
Response *composeTypes.Record
}
composeRecordSearchRestRsp struct {
Response struct {
Set composeTypes.RecordSet
}
}
wrapTest interface {
require.TestingT
Name() string
}
modelMetaMaker func(ident string) *dal.Model
attributeMaker func() dal.AttributeSet
)
const (
sysID = "ID"
sysNamespaceID = "namespaceID"
sysModuleID = "moduleID"
sysCreatedAt = "createdAt"
sysCreatedBy = "createdBy"
sysUpdatedAt = "updatedAt"
sysUpdatedBy = "updatedBy"
sysDeletedAt = "deletedAt"
sysDeletedBy = "deletedBy"
sysOwnedBy = "ownedBy"
colSysID = "id"
colSysNamespaceID = "rel_namespace"
colSysModuleID = "rel_module"
colSysCreatedAt = "created_at"
colSysCreatedBy = "created_by"
colSysUpdatedAt = "updated_at"
colSysUpdatedBy = "updated_by"
colSysDeletedAt = "deleted_at"
colSysDeletedBy = "deleted_by"
colSysOwnedBy = "owned_by"
)
func (h helper) cleanupDal() {
ds := service.DefaultStore
ctx := context.Background()
dd := dal.Service()
models, err := dd.SearchModels(ctx)
h.a.NoError(err)
for _, model := range models {
dd.Truncate(ctx, model.ToFilter(), nil)
}
h.a.NoError(store.TruncateComposeModuleFields(ctx, ds))
h.a.NoError(store.TruncateComposeModules(ctx, ds))
h.a.NoError(store.TruncateComposeNamespaces(ctx, ds))
h.a.NoError(store.TruncateDalSensitivityLevels(ctx, ds))
cc, _, err := store.SearchDalConnections(ctx, ds, types.DalConnectionFilter{})
h.a.NoError(err)
for _, c := range cc {
if c.Type == types.DalPrimaryConnectionResourceType {
continue
}
h.a.NoError(store.DeleteDalConnectionByID(ctx, ds, c.ID))
}
dd.Purge(ctx)
h.a.NoError(service.DefaultDalConnection.ReloadConnections(ctx))
h.a.NoError(composeService.DefaultModule.ReloadDALModels(ctx))
}
func initSvc(ctx context.Context, d driver) (dal.FullService, error) {
c := makeConnectionDefinition(d.dsn)
cm := dal.ConnectionConfig{
ModelIdent: c.Config.DAL.ModelIdent,
SensitivityLevelID: c.Config.Privacy.SensitivityLevelID,
Label: c.Handle,
}
svc, err := dal.New(zap.NewNop(), false)
if err != nil {
return nil, err
}
err = svc.ReplaceConnection(
ctx,
dal.MakeConnection(
c.ID,
nil,
dal.ConnectionParams{
Type: "corteza::dal:connection:dsn",
Params: map[string]any{
"dsn": d.dsn,
},
},
cm,
),
true,
)
if err != nil {
return nil, err
}
return svc, err
}
func setup(t *testing.T) (ctx context.Context, h helper, log *zap.Logger) {
log = zap.NewNop()
h = newHelperT(t)
u := &types.User{
ID: id.Next(),
}
u.SetRoles(auth.BypassRoles().IDs()...)
ctx = auth.SetIdentityToContext(context.Background(), u)
return ctx, h, log
}
func setupBench(b *testing.B) (ctx context.Context, h helper, log *zap.Logger) {
log = zap.NewNop()
h = newHelperB(b)
u := &types.User{
ID: id.Next(),
}
u.SetRoles(auth.BypassRoles().IDs()...)
ctx = auth.SetIdentityToContext(context.Background(), u)
return ctx, h, log
}
func bootstrap(rootTest *testing.T, run func(context.Context, *testing.T, helper, dal.FullService)) {
var (
ctx, h, log = setup(rootTest)
drivers = collectDrivers()
)
_ = log
for _, driver := range drivers {
rootTest.Run(driver.name, func(t *testing.T) {
if driver.dsn == "" {
t.Skip("DSN for DAL test not set")
}
svc, err := initSvc(ctx, driver)
require.NoError(t, err)
driver.setup(ctx, rootTest, driver.dsn)
run(ctx, t, h, svc)
})
}
}
func bootstrapWithModel(rootTest *testing.T, ident string, run func(context.Context, *testing.T, helper, dal.FullService, *dal.Model)) {
var (
ctx, h, log = setup(rootTest)
drivers = collectDrivers()
)
_ = log
for _, driver := range drivers {
rootTest.Run(driver.name, func(t *testing.T) {
if driver.dsn == "" {
t.Skip("DSN for DAL test not set")
}
svc, err := initSvc(ctx, driver)
require.NoError(t, err)
model := buildModel(ident, basicModelMeta, fullPartitionedSysAttributes)
driver.setup(ctx, rootTest, driver.dsn)
_, err = svc.ReplaceModel(ctx, nil, model)
require.NoError(t, err)
run(ctx, t, h, svc, model)
})
}
}
func bootstrapBenchmark(rootTest *testing.B, run func(context.Context, *testing.B, helper, dal.FullService)) {
var (
ctx, h, log = setupBench(rootTest)
drivers = collectDrivers()
)
_ = log
for _, driver := range drivers {
rootTest.Run(driver.name, func(b *testing.B) {
if driver.dsn == "" {
b.Skip("DSN for DAL test not set")
}
svc, err := initSvc(ctx, driver)
require.NoError(b, err)
driver.setup(ctx, rootTest, driver.dsn)
run(ctx, b, h, svc)
})
}
}
func collectDrivers() []driver {
return []driver{
// {
// name: "sqlite",
// dsn: "sqlite3+debug://file::memory:?cache=shared&mode=memory",
// setup: func(ctx context.Context, t *testing.T, dsn string) {
// conn, err := mysql.Setup(ctx, dsn)
// require.NoError(t, err)
// _, err = conn.Exec(loadSetupSource(t, "sqlite.sql"))
// require.NoError(t, err)
// },
// },
{
name: "mysql",
dsn: os.Getenv("DAL_TEST_DSN_MYSQL"),
setup: func(ctx context.Context, t wrapTest, dsn string) {
conn, err := mysql.Setup(ctx, dsn)
require.NoError(t, err)
_, err = conn.Exec(loadScenarioSources(t, "mysql", "sql"))
require.NoError(t, err)
},
},
// {
// name: "postgres",
// dsn: os.Getenv("DAL_TEST_DSN_POSTGRES"),
// setup: func(ctx context.Context, t *testing.T, dsn string) {
// conn, err := mysql.Setup(ctx, dsn)
// require.NoError(t, err)
// _, err = conn.Exec(loadSetupSource(t, "postgres.sql"))
// require.NoError(t, err)
// },
// },
}
}
func buildModel(ident string, mm modelMetaMaker, am attributeMaker, aa ...*dal.Attribute) *dal.Model {
out := mm(ident)
out.Attributes = aa
out.Attributes = append(out.Attributes, am()...)
return out
}
func basicModelMeta(ident string) *dal.Model {
return &dal.Model{
ConnectionID: 0,
Ident: ident,
Resource: fmt.Sprintf("testing-resource/%s", ident),
ResourceID: 0,
}
}
func fullSysAttributes() dal.AttributeSet {
return dal.AttributeSet{
dal.PrimaryAttribute(sysID, &dal.CodecAlias{Ident: colSysID}),
dal.FullAttribute(sysModuleID, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysModuleID}),
dal.FullAttribute(sysNamespaceID, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysNamespaceID}),
dal.FullAttribute(sysOwnedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysOwnedBy}),
dal.FullAttribute(sysCreatedAt, &dal.TypeTimestamp{}, &dal.CodecAlias{Ident: colSysCreatedAt}),
dal.FullAttribute(sysCreatedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysCreatedBy}),
dal.FullAttribute(sysUpdatedAt, &dal.TypeTimestamp{}, &dal.CodecAlias{Ident: colSysUpdatedAt}),
dal.FullAttribute(sysUpdatedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysUpdatedBy}),
dal.FullAttribute(sysDeletedAt, &dal.TypeTimestamp{}, &dal.CodecAlias{Ident: colSysDeletedAt}),
dal.FullAttribute(sysDeletedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysDeletedBy}),
}
}
func fullPartitionedSysAttributes() dal.AttributeSet {
return dal.AttributeSet{
dal.PrimaryAttribute(sysID, &dal.CodecAlias{Ident: colSysID}),
// mod and ns omitted here
dal.FullAttribute(sysOwnedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysOwnedBy}),
dal.FullAttribute(sysCreatedAt, &dal.TypeTimestamp{}, &dal.CodecAlias{Ident: colSysCreatedAt}),
dal.FullAttribute(sysCreatedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysCreatedBy}),
dal.FullAttribute(sysUpdatedAt, &dal.TypeTimestamp{}, &dal.CodecAlias{Ident: colSysUpdatedAt}),
dal.FullAttribute(sysUpdatedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysUpdatedBy}),
dal.FullAttribute(sysDeletedAt, &dal.TypeTimestamp{}, &dal.CodecAlias{Ident: colSysDeletedAt}),
dal.FullAttribute(sysDeletedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysDeletedBy}),
}
}
func drain(ctx context.Context, i dal.Iterator) (rr []*composeTypes.Record, err error) {
var r *composeTypes.Record
rr = make([]*composeTypes.Record, 0, 100)
for i.Next(ctx) {
if i.Err() != nil {
return nil, i.Err()
}
r = new(composeTypes.Record)
if err = i.Scan(r); err != nil {
return
}
rr = append(rr, r)
}
return rr, i.Err()
}

View File

@@ -0,0 +1,451 @@
package dal
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"time"
"github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/store"
"github.com/steinfletcher/apitest"
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/auth/handlers"
"github.com/cortezaproject/corteza/server/auth/request"
composeRest "github.com/cortezaproject/corteza/server/compose/rest"
composeService "github.com/cortezaproject/corteza/server/compose/service"
composeTypes "github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/api/server"
"github.com/cortezaproject/corteza/server/pkg/cli"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/pkg/objstore/plain"
"github.com/cortezaproject/corteza/server/pkg/rand"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers/sqlite"
"github.com/cortezaproject/corteza/server/system/rest"
"github.com/cortezaproject/corteza/server/system/service"
"github.com/cortezaproject/corteza/server/system/types"
"github.com/cortezaproject/corteza/server/tests/helpers"
"github.com/go-chi/chi/v5"
_ "github.com/joho/godotenv/autoload"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"golang.org/x/text/language"
)
type (
helper struct {
t *testing.T
b *testing.B
a *require.Assertions
cUser *types.User
roleID uint64
token []byte
}
)
var (
testApp *app.CortezaApp
r chi.Router
testUser *types.User
hh *handlers.AuthHandlers
)
func init() {
helpers.RecursiveDotEnvLoad()
}
func TestMain(m *testing.M) {
InitTestApp()
os.Exit(m.Run())
}
func InitTestApp() {
var sm *request.SessionManager
if testApp == nil {
ctx := cli.Context()
testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) {
service.CurrentSettings.Auth.External.Enabled = true
service.DefaultObjectStore, err = plain.NewWithAfero(afero.NewMemMapFs(), "test")
if err != nil {
return err
}
service.DefaultStore, err = sqlite.ConnectInMemory(ctx)
if err != nil {
return err
}
// Tests should be executed w/o any locales
locale.SetGlobal(locale.Static(&locale.Language{Tag: language.Und}))
sm = request.NewSessionManager(service.DefaultStore, app.Opt.Auth, service.DefaultLogger)
return nil
})
if err := store.TruncateUsers(context.Background(), service.DefaultStore); err != nil {
panic(fmt.Errorf("could not cleanup users: %v", err))
}
if err := testApp.Activate(ctx); err != nil {
panic(fmt.Errorf("could not activate corteza: %v", err))
}
}
hh = &handlers.AuthHandlers{
Log: zap.NewNop(),
AuthService: service.DefaultAuth,
SessionManager: sm,
}
if r == nil {
r = chi.NewRouter()
r.Use(server.BaseMiddleware(false, logger.Default())...)
helpers.BindAuthMiddleware(r)
r.Route("/system", func(r chi.Router) {
r.Group(rest.MountRoutes())
})
r.Route("/compose", func(r chi.Router) {
r.Group(composeRest.MountRoutes())
})
hh.MountHttpRoutes(r)
}
}
func newHelperT(t *testing.T) helper {
h := newHelper(t, require.New(t))
h.t = t
return h
}
func newHelperB(b *testing.B) helper {
h := newHelper(b, require.New(b))
h.b = b
return h
}
func newHelper(_ require.TestingT, a *require.Assertions) helper {
var (
h = helper{
a: a,
roleID: id.Next(),
}
ctx = context.Background()
)
if testUser == nil {
testUser = &types.User{
Handle: "test_user",
Name: "test_user",
ID: id.Next(),
CreatedAt: time.Now(),
}
err := store.CreateUser(ctx, service.DefaultStore, testUser)
if err != nil {
panic(err)
}
}
h.cUser = testUser
h.cUser.SetRoles(h.roleID)
helpers.UpdateRBAC(h.roleID)
h.identityToHelper(ctx, h.cUser)
h.mockPermissionsWithAccess()
return h
}
// apiInit basics, initialize, set handler, add auth
func (h helper) apiInit() *apitest.APITest {
InitTestApp()
return apitest.
New().
Handler(r).
Intercept(helpers.ReqHeaderRawAuthBearer(h.token))
}
func (h helper) MyRole() uint64 {
return h.roleID
}
// Returns context w/ security details
func (h helper) secCtx() context.Context {
return auth.SetIdentityToContext(context.Background(), h.cUser)
}
func (h *helper) identityToHelper(ctx context.Context, u *types.User) {
var err error
h.cUser = u
h.token, err = auth.TokenIssuer.Issue(ctx, auth.WithIdentity(h.cUser))
if err != nil {
panic(err)
}
}
func (h helper) mockPermissions(rules ...*rbac.Rule) {
h.a.NoError(rbac.Global().Grant(
// TestService we use does not have any backend storage,
context.Background(),
rules...,
))
}
// Prepends allow access rule for system service for everyone
func (h helper) mockPermissionsWithAccess(rules ...*rbac.Rule) {
h.mockPermissions(rules...)
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Resource utilities
func (h helper) createNamespace(name string) *composeTypes.Namespace {
ns := &composeTypes.Namespace{Name: name, Slug: name}
ns.ID = id.Next()
ns.CreatedAt = time.Now()
h.a.NoError(store.CreateComposeNamespace(context.Background(), service.DefaultStore, ns))
return ns
}
func (h helper) createSensitivityLevel(res *types.DalSensitivityLevel) *types.DalSensitivityLevel {
if res.ID == 0 {
res.ID = id.Next()
}
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
h.a.NoError(service.DefaultStore.CreateDalSensitivityLevel(context.Background(), res))
h.a.NoError(service.DefaultDalSensitivityLevel.ReloadSensitivityLevels(context.Background(), service.DefaultStore))
return res
}
func (h helper) createDalConnection(res *types.DalConnection) *types.DalConnection {
if res.ID == 0 {
res.ID = id.Next()
}
if res.Meta.Name == "" {
res.Meta.Name = "Test Connection"
}
if res.Handle == "" {
res.Handle = "test_connection"
}
if res.Type == "" {
res.Type = types.DalConnectionResourceType
}
if res.Meta.Ownership == "" {
res.Meta.Ownership = "tester"
}
if res.Config.DAL == nil {
res.Config.DAL = &types.ConnectionConfigDAL{
Type: "corteza::dal:connection:dsn",
Params: map[string]any{
"dsn": "sqlite3://file::memory:?cache=shared&mode=memory",
},
}
}
if res.Config.DAL.ModelIdent == "" {
res.Config.DAL.ModelIdent = "compose_records"
}
if res.CreatedAt.IsZero() {
res.CreatedAt = time.Now()
}
if res.CreatedBy == 0 {
res.CreatedBy = h.cUser.ID
}
h.a.NoError(service.DefaultStore.CreateDalConnection(context.Background(), res))
h.a.NoError(service.DefaultDalConnection.ReloadConnections(context.Background()))
return res
}
func (h helper) getPrimaryConnection() *types.DalConnection {
cc, _, err := store.SearchDalConnections(context.Background(), service.DefaultStore, types.DalConnectionFilter{Type: types.DalPrimaryConnectionResourceType})
h.a.NoError(err)
if len(cc) != 1 {
h.a.FailNow("invalid state: no or too many primary connections")
}
return cc[0]
}
func makeConnectionDefinition(dsn string) *types.DalConnection {
return &types.DalConnection{
ID: id.Next(),
Type: types.DalConnectionResourceType,
Config: types.ConnectionConfig{
DAL: &types.ConnectionConfigDAL{
ModelIdent: "compose_record",
},
},
}
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utilities
// ---
func loadRequestFromScenario(t *testing.T, name string) string {
return loadRequestFrom(t, suiteFromT(t), name)
}
func loadRequestFromGenerics(t *testing.T, name string) string {
return loadRequestFrom(t, "generic", name)
}
func loadRequestFrom(t *testing.T, suite, name string) string {
f, err := os.Open(path.Join("testdata", suite, name))
require.NoError(t, err)
defer f.Close()
bb, err := ioutil.ReadAll(f)
require.NoError(t, err)
return string(bb)
}
func loadRequestFromScenarioWithConnection(t *testing.T, req string, connectionID uint64) string {
out := loadRequestFromScenario(t, req)
aux := &composeTypes.Module{}
require.NoError(t, json.Unmarshal([]byte(out), &aux))
aux.Config.DAL.ConnectionID = connectionID
a, err := json.Marshal(aux)
require.NoError(t, err)
return string(a)
}
func createRecordFromCase(ctx context.Context, t *testing.T, name string, namespaceID, moduleID uint64) (record *composeTypes.Record) {
return createRecordFrom(ctx, t, suiteFromT(t), name, namespaceID, moduleID)
}
func createRecordFromGenerics(ctx context.Context, t *testing.T, name string, namespaceID, moduleID uint64) (record *composeTypes.Record) {
return createRecordFrom(ctx, t, "generic", name, namespaceID, moduleID)
}
func createRecordFrom(ctx context.Context, t *testing.T, suite, name string, namespaceID, moduleID uint64) (record *composeTypes.Record) {
raw := loadRequestFrom(t, suite, name)
record = &composeTypes.Record{}
require.NoError(t, json.Unmarshal([]byte(raw), &record))
record.NamespaceID = namespaceID
record.ModuleID = moduleID
record, _, err := composeService.DefaultRecord.Create(ctx, record)
require.NoError(t, err)
return record
}
func createModuleFromCase(ctx context.Context, t *testing.T, name string, namespaceID uint64, config *composeTypes.ModuleConfig) (module *composeTypes.Module) {
return createModuleFrom(ctx, t, suiteFromT(t), name, namespaceID, config)
}
func createModuleFromGenerics(ctx context.Context, t *testing.T, name string, namespaceID uint64, config *composeTypes.ModuleConfig) (module *composeTypes.Module) {
return createModuleFrom(ctx, t, "generic", name, namespaceID, config)
}
func createModuleFrom(ctx context.Context, t *testing.T, suite, name string, namespaceID uint64, config *composeTypes.ModuleConfig) (module *composeTypes.Module) {
raw := loadRequestFrom(t, suite, name)
module = &composeTypes.Module{}
require.NoError(t, json.Unmarshal([]byte(raw), &module))
module.NamespaceID = namespaceID
if config != nil {
// let's be careful not to override whole config
module.Config.DAL = config.DAL
}
module, err := composeService.DefaultModule.Create(ctx, module)
require.NoError(t, err)
return module
}
func createConnectionFromCase(ctx context.Context, t *testing.T, name string) (connection *types.DalConnection) {
return createConnectionFrom(ctx, t, suiteFromT(t), name)
}
func createConnectionFromGenerics(ctx context.Context, t *testing.T, name string) (connection *types.DalConnection) {
return createConnectionFrom(ctx, t, "generic", name)
}
func createConnectionFrom(ctx context.Context, t *testing.T, suite, name string) (connection *types.DalConnection) {
raw := loadRequestFrom(t, suite, name)
aux := &types.DalConnection{}
require.NoError(t, json.Unmarshal([]byte(raw), &aux))
connection, err := service.DefaultDalConnection.Create(ctx, aux)
require.NoError(t, err)
return connection
}
// ---
func suiteFromT(t *testing.T) string {
return t.Name()[5:]
}
// random string, 10 chars long by default
func rs(a ...int) string {
var l = 10
if len(a) > 0 {
l = a[0]
}
return string(rand.Bytes(l))
}
func loadScenarioSources(t wrapTest, driver, ext string) (src string) {
scenarioName := t.Name()[5:]
src = loadScenarioSource(t, "generic", fmt.Sprintf("_.%s", ext))
src += "\n"
src += loadScenarioSource(t, "generic", fmt.Sprintf("%s.%s", driver, ext))
src += "\n"
src += loadScenarioSource(t, scenarioName, fmt.Sprintf("_.%s", ext))
src += "\n"
src += loadScenarioSource(t, scenarioName, fmt.Sprintf("%s.%s", driver, ext))
return
}
func loadScenarioSource(t wrapTest, scenarioName, srcName string) (src string) {
f, err := os.Open(path.Join("testdata", scenarioName, srcName))
if err != nil && os.IsNotExist(err) {
return ""
}
require.NoError(t, err)
defer f.Close()
out, err := ioutil.ReadAll(bufio.NewReader(f))
require.NoError(t, err)
return string(out)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
package mysql
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers/mysql"
"github.com/jmoiron/sqlx"
)
func Setup(ctx context.Context, dsn string) (_ *sqlx.DB, err error) {
var (
cfg *rdbms.ConnConfig
)
cfg, err = mysql.NewConfig(dsn)
if err != nil {
return
}
return rdbms.Connect(ctx, logger.MakeDebugLogger(), cfg)
}

View File

@@ -0,0 +1,23 @@
package postgres
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers/postgres"
"github.com/jmoiron/sqlx"
)
func Setup(ctx context.Context, dsn string) (_ *sqlx.DB, err error) {
var (
cfg *rdbms.ConnConfig
)
cfg, err = postgres.NewConfig(dsn)
if err != nil {
return
}
return rdbms.Connect(ctx, logger.MakeDebugLogger(), cfg)
}

View File

@@ -0,0 +1,23 @@
package sqlite
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/logger"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms"
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers/sqlite"
"github.com/jmoiron/sqlx"
)
func Setup(ctx context.Context, dsn string) (_ *sqlx.DB, err error) {
var (
cfg *rdbms.ConnConfig
)
cfg, err = sqlite.NewConfig(dsn)
if err != nil {
return
}
return rdbms.Connect(ctx, logger.MakeDebugLogger(), cfg)
}

View File

@@ -0,0 +1,48 @@
{
"handle": "primary_connection",
"type": "corteza::system:primary-dal-connection",
"meta": {
"name": "Primary Connection EDITED",
"location": {
"geometry": {
"type": "",
"coordinates": null
},
"properties": {
"name": ""
}
},
"ownership": ""
},
"config": {
"connection": {
"type": "corteza::dal:connection:dsn",
"params": {
"dsn": "sqlite3://file::memory:?cache=shared&mode=memory"
}
},
"privacy": {
"sensitivityLevelID": "0"
},
"dal": {
"properties": {},
"operations": [
"corteza::dal:operation:create",
"corteza::dal:operation:update",
"corteza::dal:operation:search",
"corteza::dal:operation:lookup",
"corteza::dal:operation:paging",
"corteza::dal:operation:stats",
"corteza::dal:operation:sorting",
"corteza::dal:operation:RBAC"
],
"modelIdent": "compose_record",
"attributeIdent": "values",
"partitionFormat": "compose_record_{{namespace}}_{{module}}",
"partitionIdentValidator": ""
}
}
}

View File

@@ -0,0 +1,34 @@
{
"name": "internal_non_partitioned",
"handle": "internal_non_partitioned",
"config": {
"dal": {
"connectionID": "42",
"operations": [],
"partitioned": false
},
"privacy": {
"sensitivityLevelID": "0",
"usageDisclosure": "A"
}
},
"fields": [
{
"name": "f1",
"kind": "String",
"privacy": {
"sensitivityLevelID": "0",
"usageDisclosure": "A"
}
},
{
"name": "f2",
"kind": "String"
}
]
}

48
server/tests/dal/testdata/generic/_.sql vendored Normal file
View File

@@ -0,0 +1,48 @@
DROP TABLE IF EXISTS compose_record;
CREATE TABLE `compose_record` (
`id` bigint unsigned NOT NULL,
`rel_namespace` bigint unsigned NOT NULL,
`module_id` bigint unsigned NOT NULL,
`values` json DEFAULT NULL,
`owned_by` bigint unsigned NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
`created_by` bigint unsigned NOT NULL,
`updated_by` bigint unsigned NOT NULL DEFAULT '0',
`deleted_by` bigint unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `compose_record_owner` (`owned_by`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
DROP TABLE IF EXISTS compose_record_partitioned;
CREATE TABLE `compose_record_partitioned` (
`id` bigint unsigned NOT NULL,
`vID` BIGINT UNSIGNED,
`vRef` BIGINT UNSIGNED,
`vTimestamp` DATETIME,
`vTime` TIME,
`vDate` DATE,
`vNumber` NUMERIC,
`vText` TEXT,
`vBoolean_T` BOOLEAN,
`vBoolean_F` BOOLEAN,
`vEnum` TEXT,
`vGeometry` TEXT,
`vJSON` TEXT,
`vBlob` BLOB,
`vUUID` VARCHAR(36),
`owned_by` bigint unsigned NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
`created_by` bigint unsigned NOT NULL,
`updated_by` bigint unsigned NOT NULL DEFAULT '0',
`deleted_by` bigint unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `compose_record_owner` (`owned_by`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

View File

@@ -0,0 +1,32 @@
{
"handle": "test_connection",
"type": "corteza::system:dal-connection",
"meta": {
"name": "Test Connection",
"location": {
"geometry": {
"type": "",
"coordinates": null
},
"properties": {
"name": ""
}
},
"ownership": ""
},
"config": {
"dal": {
"modelIdent": "compose_record",
"type": "corteza::dal:connection:dsn",
"params": {
"dsn": "NO"
}
},
"privacy": {
"sensitivityLevelID": "0"
}
}
}

View File

@@ -0,0 +1,32 @@
{
"handle": "test_connection",
"type": "corteza::system:primary-dal-connection",
"meta": {
"name": "Test Connection",
"location": {
"geometry": {
"type": "",
"coordinates": null
},
"properties": {
"name": ""
}
},
"ownership": ""
},
"config": {
"dal": {
"modelIdent": "compose_record",
"type": "corteza::dal:connection:dsn",
"params": {
"dsn": "sqlite3://file::memory:?cache=shared&mode=memory"
}
},
"privacy": {
"sensitivityLevelID": "0"
}
}
}

View File

@@ -0,0 +1,32 @@
{
"handle": "test_connection",
"type": "corteza::system:dal-connection",
"meta": {
"name": "Test Connection",
"location": {
"geometry": {
"type": "",
"coordinates": null
},
"properties": {
"name": ""
}
},
"ownership": ""
},
"config": {
"dal": {
"modelIdent": "compose_record",
"type": "corteza::dal:connection:dsn",
"params": {
"dsn": "sqlite3://file::memory:?cache=shared&mode=memory"
}
},
"privacy": {
"sensitivityLevelID": "42"
}
}
}

View File

@@ -0,0 +1,38 @@
{
"name": "internal_non_partitioned",
"handle": "internal_non_partitioned",
"config": {
"dal": {
"connectionID": "0",
"operations": [],
"partitioned": false
},
"privacy": {
"sensitivityLevelID": "0",
"usageDisclosure": "A"
}
},
"fields": [
{
"name": "name",
"kind": "String",
"config": {
"privacy": {
"sensitivityLevelID": "42",
"usageDisclosure": "A"
}
}
},
{
"name": "email",
"kind": "Email"
},
{
"name": "a_number",
"kind": "Number"
}
]
}

View File

@@ -0,0 +1,36 @@
{
"name": "internal_non_partitioned",
"handle": "internal_non_partitioned",
"config": {
"dal": {
"connectionID": "0",
"operations": [],
"partitioned": false
},
"privacy": {
"sensitivityLevelID": "42",
"usageDisclosure": "A"
}
},
"fields": [
{
"name": "name",
"kind": "String",
"privacy": {
"sensitivityLevelID": "0",
"usageDisclosure": "A"
}
},
{
"name": "email",
"kind": "Email"
},
{
"name": "a_number",
"kind": "Number"
}
]
}

View File

@@ -0,0 +1,37 @@
{
"name": "internal_non_partitioned",
"handle": "internal_non_partitioned",
"config": {
"dal": {
"connectionID": "0",
"operations": [],
"partitioned": false
},
"privacy": {
"sensitivityLevelID": "42",
"usageDisclosure": "A"
}
},
"fields": [
{
"name": "name",
"kind": "String",
"privacy": {
"sensitivityLevelID": "0",
"usageDisclosure": "A"
}
},
{
"name": "email",
"kind": "Email"
},
{
"name": "a_number",
"kind": "Number"
}
]
}

View File

@@ -0,0 +1,32 @@
{
"handle": "test_connection",
"type": "corteza::system:dal-connection",
"meta": {
"name": "Test Connection",
"location": {
"geometry": {
"type": "",
"coordinates": null
},
"properties": {
"name": ""
}
},
"ownership": ""
},
"config": {
"dal": {
"modelIdent": "compose_record",
"type": "corteza::dal:connection:dsn",
"params": {
"dsn": "sqlite3://file::memory:?cache=shared&mode=memory"
}
},
"privacy": {
"sensitivityLevelID": "0"
}
}
}

View File

@@ -0,0 +1,32 @@
{
"handle": "test_connection_edited",
"type": "corteza::system:dal-connection",
"meta": {
"name": "Test Connection EDITED",
"location": {
"geometry": {
"type": "",
"coordinates": null
},
"properties": {
"name": ""
}
},
"ownership": ""
},
"config": {
"dal": {
"modelIdent": "compose_record",
"type": "corteza::dal:connection:dsn",
"params": {
"dsn": "sqlite3://file::memory:?cache=shared&mode=memory"
}
},
"privacy": {
"sensitivityLevelID": "0"
}
}
}

View File

@@ -0,0 +1,31 @@
{
"name": "internal_non_partitioned",
"handle": "internal_non_partitioned",
"config": {
"dal": {
"connectionID": "0",
"operations": [],
"partitioned": false
},
"privacy": {
"sensitivityLevelID": "0",
"usageDisclosure": "A"
}
},
"fields": [
{
"name": "name",
"kind": "String"
},
{
"name": "email",
"kind": "Email"
},
{
"name": "a_number",
"kind": "Number"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"values": [
{
"name": "name",
"value": "Me"
},
{
"name": "email",
"value": "me@test.tld"
},
{
"name": "a_number",
"value": "42"
}
]
}

View File

@@ -0,0 +1,16 @@
{
"values": [
{
"name": "name",
"value": "Me updated"
},
{
"name": "email",
"value": "me+updated@test.tld"
},
{
"name": "a_number",
"value": "43"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"handle": "private",
"level": 1,
"meta": {
"name": "private sensitivity",
"description": "data is private to the owner"
}
}

View File

@@ -0,0 +1,8 @@
{
"handle": "private_edited",
"level": 1,
"meta": {
"name": "private sensitivity",
"description": "data is private to the owner"
}
}

View File

@@ -0,0 +1,388 @@
package envoy
import (
"context"
"fmt"
"os"
"testing"
automationTypes "github.com/cortezaproject/corteza/server/automation/types"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/require"
)
func TestImportExport(t *testing.T) {
var (
ctx = context.Background()
req = require.New(t)
nodes envoyx.NodeSet
gg *envoyx.DepGraph
err error
)
cleanup(t)
// The test
//
// * imports some YAML files
// * checks the DB state
// * exports the DB into a YAML
// * clears the DB
// * imports the YAML
// * checks the DB state the same way as before
//
// The above outlined flow allows us to trivially check if the data is both
// imported and exported correctly.
//
// The initial step could also manually populate the DB but the YAML import
// is more convenient.
t.Run("initial import", func(t *testing.T) {
t.Run("parse configs", func(t *testing.T) {
nodes, _, err = defaultEnvoy.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeURI,
Params: map[string]any{
"uri": "file://testdata/full",
},
})
req.NoError(err)
})
t.Run("bake", func(t *testing.T) {
gg, err = defaultEnvoy.Bake(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": defaultStore,
"dal": defaultDal,
},
}, nil, nodes...)
req.NoError(err)
})
t.Run("import into DB", func(t *testing.T) {
err = defaultEnvoy.Encode(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": defaultStore,
"dal": defaultDal,
},
}, gg)
req.NoError(err)
})
assertFullState(ctx, t, defaultStore, req)
})
// Prepare a temp file where we'll dump the YAML into
auxFile, err := os.CreateTemp(os.TempDir(), "*.yaml")
req.NoError(err)
spew.Dump(auxFile.Name())
// defer os.Remove(auxFile.Name())
defer auxFile.Close()
t.Run("export", func(t *testing.T) {
t.Run("export from DB", func(t *testing.T) {
nodes, _, err = defaultEnvoy.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeStore,
Params: map[string]any{
"storer": defaultStore,
"dal": defaultDal,
},
Filter: map[string]envoyx.ResourceFilter{
types.ChartResourceType: {},
types.ModuleResourceType: {},
types.NamespaceResourceType: {},
types.PageResourceType: {},
types.PageLayoutResourceType: {},
systemTypes.ApplicationResourceType: {},
systemTypes.ApigwRouteResourceType: {},
systemTypes.AuthClientResourceType: {},
systemTypes.QueueResourceType: {},
systemTypes.ReportResourceType: {},
systemTypes.RoleResourceType: {},
systemTypes.TemplateResourceType: {},
systemTypes.UserResourceType: {},
systemTypes.DalConnectionResourceType: {},
systemTypes.DalSensitivityLevelResourceType: {},
automationTypes.WorkflowResourceType: {},
},
})
req.NoError(err)
})
t.Run("bake", func(t *testing.T) {
gg, err = defaultEnvoy.Bake(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": defaultStore,
"dal": defaultDal,
},
}, nil, nodes...)
req.NoError(err)
})
t.Run("write file", func(t *testing.T) {
err = defaultEnvoy.Encode(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeIo,
Params: map[string]any{
"writer": auxFile,
},
}, gg)
req.NoError(err)
})
})
cleanup(t)
t.Run("second import", func(t *testing.T) {
t.Run("yaml parse", func(t *testing.T) {
nodes, _, err = defaultEnvoy.Decode(ctx, envoyx.DecodeParams{
Type: envoyx.DecodeTypeURI,
Params: map[string]any{
"uri": fmt.Sprintf("file://%s", auxFile.Name()),
},
})
req.NoError(err)
})
t.Run("bake", func(t *testing.T) {
gg, err = defaultEnvoy.Bake(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": defaultStore,
"dal": defaultDal,
},
}, nil, nodes...)
req.NoError(err)
})
t.Run("run import", func(t *testing.T) {
err = defaultEnvoy.Encode(ctx, envoyx.EncodeParams{
Type: envoyx.EncodeTypeStore,
Params: map[string]any{
"storer": defaultStore,
"dal": defaultDal,
},
}, gg)
req.NoError(err)
})
assertFullState(ctx, t, defaultStore, req)
})
}
func assertFullState(ctx context.Context, t *testing.T, s store.Storer, req *require.Assertions) {
t.Run("check state", func(t *testing.T) {
t.Run("corteza::compose", func(t *testing.T) {
// Namespaces
ns, err := store.LookupComposeNamespaceBySlug(ctx, s, "test_ns_1")
req.NoError(err)
req.NotNil(ns)
// Modules
modules, _, err := store.SearchComposeModules(ctx, s, types.ModuleFilter{
NamespaceID: ns.ID,
Sorting: filter.Sorting{
Sort: filter.SortExprSet{{Column: "handle", Descending: false}},
},
})
req.NoError(err)
mod1 := modules.FindByHandle("test_ns_1_mod_1")
req.NotNil(mod1)
mod2 := modules.FindByHandle("test_ns_1_mod_2")
req.NotNil(mod2)
// Fields
fields, _, err := store.SearchComposeModuleFields(ctx, s, types.ModuleFieldFilter{
ModuleID: []uint64{mod1.ID, mod2.ID},
})
req.NoError(err)
assignModuleFields(modules, fields)
req.Len(mod1.Fields, 2)
req.Len(mod2.Fields, 2)
req.Equal(mod1.ID, mod2.Fields.FindByName("test_ns_1_mod_2_f1").Options.UInt64("moduleID"))
req.Equal(mod2.ID, mod2.Fields.FindByName("test_ns_1_mod_2_f2").Options.UInt64("moduleID"))
// Charts
charts, _, err := store.SearchComposeCharts(ctx, s, types.ChartFilter{
NamespaceID: ns.ID,
})
req.NoError(err)
ch1 := charts.FindByHandle("test_ns_1_chart_1")
req.NotNil(ch1)
req.Equal(mod1.ID, ch1.Config.Reports[0].ModuleID)
req.Equal(mod2.ID, ch1.Config.Reports[1].ModuleID)
// Pages
pages, _, err := store.SearchComposePages(ctx, s, types.PageFilter{
NamespaceID: ns.ID,
})
req.NoError(err)
pg1 := pages.FindByHandle("test_ns_1_page_1")
req.NotNil(pg1)
req.Len(pg1.Blocks, 8)
// @todo test page block references
// spew.Dump(pg1.Blocks)
rpg1 := pages.FindByHandle("test_ns_1_record_page_1")
req.NotNil(rpg1)
req.Equal(mod1.ID, rpg1.ModuleID)
// Page layouts
layouts, _, err := store.SearchComposePageLayouts(ctx, s, types.PageLayoutFilter{
NamespaceID: ns.ID,
})
req.NoError(err)
ly1 := layouts.FindByHandle("test_ns_1_page_1_layout_1")
req.NotNil(ly1)
ly2 := layouts.FindByHandle("test_ns_1_page_1_layout_2")
req.NotNil(ly2)
// Record page layouts
ly3 := layouts.FindByHandle("test_ns_1_record_page_1_layout_1")
req.NotNil(ly3)
ly4 := layouts.FindByHandle("test_ns_1_record_page_1_layout_2")
req.NotNil(ly4)
})
t.Run("corteza::system", func(t *testing.T) {
// Users
users, _, err := store.SearchUsers(ctx, s, systemTypes.UserFilter{})
req.NoError(err)
req.Len(users, 1)
u1 := users[0]
req.Equal("test_user_1", u1.Handle)
// ApigwRoutes
apigwroutes, _, err := store.SearchApigwRoutes(ctx, s, systemTypes.ApigwRouteFilter{})
req.NoError(err)
req.Len(apigwroutes, 1)
r1 := apigwroutes[0]
req.Equal("/test/endpoint/1", r1.Endpoint)
req.Equal(u1.ID, r1.CreatedBy)
req.Equal(u1.ID, r1.UpdatedBy)
// ApigwFilters
apigwfilter, _, err := store.SearchApigwFilters(ctx, s, systemTypes.ApigwFilterFilter{})
req.NoError(err)
req.Len(apigwfilter, 1)
f1 := apigwfilter[0]
req.Equal(r1.ID, f1.Route)
// AuthClients
clients, _, err := store.SearchAuthClients(ctx, s, systemTypes.AuthClientFilter{})
req.NoError(err)
req.Len(clients, 1)
c1 := clients[0]
req.Equal("test_authclient_1", c1.Handle)
req.Equal(u1.ID, c1.OwnedBy)
req.Equal(u1.ID, c1.CreatedBy)
req.Equal(u1.ID, c1.UpdatedBy)
// Queues
queues, _, err := store.SearchQueues(ctx, s, systemTypes.QueueFilter{})
req.NoError(err)
req.Len(queues, 1)
q1 := queues[0]
req.Equal("test_queue_1", q1.Queue)
req.Equal(u1.ID, q1.CreatedBy)
req.Equal(u1.ID, q1.UpdatedBy)
// Reports
reports, _, err := store.SearchReports(ctx, s, systemTypes.ReportFilter{})
req.NoError(err)
req.Len(reports, 1)
rp1 := reports[0]
req.Equal("test_report_1", rp1.Handle)
req.Equal(u1.ID, rp1.OwnedBy)
req.Equal(u1.ID, rp1.CreatedBy)
req.Equal(u1.ID, rp1.UpdatedBy)
// Roles
roles, _, err := store.SearchRoles(ctx, s, systemTypes.RoleFilter{})
req.NoError(err)
rl1 := roles[0]
req.Equal("test_role_1", rl1.Handle)
// Templates
templates, _, err := store.SearchTemplates(ctx, s, systemTypes.TemplateFilter{})
req.NoError(err)
req.Len(templates, 1)
tpl1 := templates[0]
req.Equal("test_template_1", tpl1.Handle)
req.Equal(u1.ID, tpl1.OwnerID)
// DalConnections
connections, _, err := store.SearchDalConnections(ctx, s, systemTypes.DalConnectionFilter{})
req.NoError(err)
req.Len(connections, 1)
conn1 := connections[0]
req.Equal("test_connection_1", conn1.Handle)
req.Equal(u1.ID, conn1.CreatedBy)
req.Equal(u1.ID, conn1.UpdatedBy)
// DalSensitivityLevels
dalsensitivitylevels, _, err := store.SearchDalSensitivityLevels(ctx, s, systemTypes.DalSensitivityLevelFilter{})
req.NoError(err)
req.Len(dalsensitivitylevels, 1)
sens1 := dalsensitivitylevels[0]
req.Equal("test_sensitivity_level_1", sens1.Handle)
req.Equal(u1.ID, sens1.CreatedBy)
req.Equal(u1.ID, sens1.UpdatedBy)
})
t.Run("corteza::automation", func(t *testing.T) {
// Workflows
workflows, _, err := store.SearchAutomationWorkflows(ctx, s, automationTypes.WorkflowFilter{})
req.NoError(err)
req.Len(workflows, 1)
wf1 := workflows[0]
req.Equal("test_workflow_1", wf1.Handle)
// Triggers
triggers, _, err := store.SearchAutomationTriggers(ctx, s, automationTypes.TriggerFilter{})
req.NoError(err)
req.Len(triggers, 1)
t1 := triggers[0]
req.Equal(wf1.ID, t1.WorkflowID)
})
})
}
func assignModuleFields(modules types.ModuleSet, fields types.ModuleFieldSet) {
for _, m := range modules {
for _, f := range fields {
if m.ID == f.ModuleID {
m.Fields = append(m.Fields, f)
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More