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

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