diff --git a/server/compose/page.cue b/server/compose/page.cue index b250d6a09..4c2a82e05 100644 --- a/server/compose/page.cue +++ b/server/compose/page.cue @@ -59,7 +59,7 @@ page: { } meta: { - goType: "types.PageMeta" + goType: "*types.PageMeta" dal: { type: "JSON", defaultEmptyObject: true } } config: { diff --git a/server/compose/service/page_layout.go b/server/compose/service/page_layout.go index 5a749ffc1..b6e9f6fb5 100644 --- a/server/compose/service/page_layout.go +++ b/server/compose/service/page_layout.go @@ -122,7 +122,7 @@ func (svc pageLayout) search(ctx context.Context, filter types.PageLayoutFilter) filter.Check = checkPageLayout(ctx, svc.ac) err = func() error { - ns, pg, err = loadPageCombo(ctx, svc.store, ns.ID, filter.PageID) + ns, pg, err = loadPageCombo(ctx, svc.store, filter.NamespaceID, filter.PageID) if err != nil { return err } diff --git a/server/compose/types/page.go b/server/compose/types/page.go index 4adc22f98..8ada6d26a 100644 --- a/server/compose/types/page.go +++ b/server/compose/types/page.go @@ -418,6 +418,9 @@ func (set PageSet) FindByHandle(handle string) *Page { func (bb *PageBlocks) Scan(src any) error { return sql.ParseJSON(src, bb) } func (bb PageBlocks) Value() (driver.Value, error) { return json.Marshal(bb) } +func (bb *PageMeta) Scan(src any) error { return sql.ParseJSON(src, bb) } +func (bb PageMeta) Value() (driver.Value, error) { return json.Marshal(bb) } + // Helper to extract old encoding to new one func (b *PageBlock) UnmarshalJSON(data []byte) (err error) { type internalPageBlock PageBlock diff --git a/server/store/adapters/rdbms/aux_types.gen.go b/server/store/adapters/rdbms/aux_types.gen.go index f300e134b..73e880d32 100644 --- a/server/store/adapters/rdbms/aux_types.gen.go +++ b/server/store/adapters/rdbms/aux_types.gen.go @@ -293,7 +293,7 @@ type ( SelfID uint64 `db:"self_id"` ModuleID uint64 `db:"module_id"` NamespaceID uint64 `db:"namespace_id"` - Meta composeType.PageMeta `db:"meta"` + Meta *composeType.PageMeta `db:"meta"` Config composeType.PageConfig `db:"config"` Blocks composeType.PageBlocks `db:"blocks"` Visible bool `db:"visible"` diff --git a/server/tests/compose/page_layout_test.go b/server/tests/compose/page_layout_test.go new file mode 100644 index 000000000..db7f574d1 --- /dev/null +++ b/server/tests/compose/page_layout_test.go @@ -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, name string) *types.PageLayout { + res := &types.PageLayout{ + ID: id.Next(), + CreatedAt: time.Now(), + Meta: &types.PageLayoutMeta{ + Name: name, + }, + 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.name`, ly.Meta.Name)). + 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.name=="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": {"name": "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": {"name": "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.Name) +} + +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": {"name": "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": {"name": "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.Name) +} + +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) +} diff --git a/server/tests/compose/page_test.go b/server/tests/compose/page_test.go index e890292d6..03d605017 100644 --- a/server/tests/compose/page_test.go +++ b/server/tests/compose/page_test.go @@ -16,7 +16,7 @@ import ( "github.com/cortezaproject/corteza/server/store" systemService "github.com/cortezaproject/corteza/server/system/service" "github.com/cortezaproject/corteza/server/tests/helpers" - "github.com/steinfletcher/apitest-jsonpath" + jsonpath "github.com/steinfletcher/apitest-jsonpath" "github.com/stretchr/testify/require" ) @@ -29,6 +29,7 @@ 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, } @@ -52,6 +53,7 @@ func (h helper) repoMakeWeightedPage(ns *types.Namespace, name string, weight in res := &types.Page{ ID: id.Next(), CreatedAt: time.Now(), + Meta: &types.PageMeta{}, Title: name, NamespaceID: ns.ID, Weight: weight,