package compose import ( "bytes" "context" "fmt" "mime/multipart" "net/http" "os" "strconv" "testing" "time" "github.com/cortezaproject/corteza/server/compose/dalutils" "github.com/cortezaproject/corteza/server/compose/service/values" "github.com/cortezaproject/corteza/server/compose/types" "github.com/cortezaproject/corteza/server/pkg/id" systemService "github.com/cortezaproject/corteza/server/system/service" "github.com/cortezaproject/corteza/server/tests/helpers" "github.com/steinfletcher/apitest" jsonpath "github.com/steinfletcher/apitest-jsonpath" "github.com/stretchr/testify/require" ) func (h helper) clearRecords() { h.clearNamespaces() h.clearModules() h.noError(truncateRecords(context.Background())) } type ( rImportSession struct { Response struct { SessionID string `json:"sessionID"` } `json:"response"` } ) func (h helper) makeRecordModuleWithFieldsOnNs(name string, namespace *types.Namespace, ff ...*types.ModuleField) *types.Module { helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read") helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") if len(ff) == 0 { // Default fields ff = types.ModuleFieldSet{ &types.ModuleField{ Name: "name", }, &types.ModuleField{ Name: "email", }, &types.ModuleField{ Name: "options", Multi: true, }, &types.ModuleField{ Name: "description", }, &types.ModuleField{ Name: "another_record", Kind: "Record", }, } } return h.makeModule(namespace, name, ff...) } func (h helper) repoMakeRecordModuleWithFields(name string, ff ...*types.ModuleField) *types.Module { namespace := h.makeNamespace("record testing namespace") helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update") if len(ff) == 0 { // Default fields ff = types.ModuleFieldSet{ &types.ModuleField{ Name: "name", }, &types.ModuleField{ Name: "email", }, &types.ModuleField{ Name: "options", Multi: true, }, &types.ModuleField{ Name: "description", }, &types.ModuleField{ Name: "another_record", Kind: "Record", }, } } return h.makeModule(namespace, name, ff...) } func (h helper) repoMakeRecordModuleWithFieldsRequired(name string, ff ...*types.ModuleField) *types.Module { namespace := h.makeNamespace("record testing namespace") helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update") if len(ff) == 0 { // Default fields ff = types.ModuleFieldSet{ &types.ModuleField{ Name: "name", Required: true, }, &types.ModuleField{ Name: "email", }, &types.ModuleField{ Name: "options", Multi: true, }, &types.ModuleField{ Name: "description", }, &types.ModuleField{ Name: "another_record", Kind: "Record", }, } } return h.makeModule(namespace, name, ff...) } func (h helper) makeRecord(module *types.Module, rvs ...*types.RecordValue) *types.Record { recID := id.Next() for _, rv := range rvs { rv.RecordID = recID } rec := &types.Record{ ID: recID, CreatedAt: time.Now(), ModuleID: module.ID, NamespaceID: module.NamespaceID, // Passing the current owner in here since the tests (who care about this) // rely on it being set to something valid. OwnedBy: h.cUser.ID, // We are directly storing the record values here, so ensure // everything is formatted in the same manner as it would be // when stored through the service Values: values.Formatter().Run(module, rvs), } rec.SetModule(module) h.noError(dalutils.ComposeRecordCreate(context.Background(), defDal, module, rec)) return rec } func (h helper) lookupRecordByID(module *types.Module, ID uint64) *types.Record { res, err := dalutils.ComposeRecordsFind(context.Background(), defDal, module, ID) h.noError(err) return res } func TestRecordRead(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module) h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). Assert(jsonpath.Equal(`$.response.recordID`, fmt.Sprintf("%d", record.ID))). End() } func TestRecordList(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") helpers.AllowMe(h, module.RbacResource(), "records.search") h.makeRecord(module) h.makeRecord(module) h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). Assert(jsonpath.Len(`$.response.set`, 2)). End() } func TestRecordListWithTotal(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") helpers.AllowMe(h, module.RbacResource(), "records.search") for i := 0; i < 7; i++ { h.makeRecord(module, &types.RecordValue{Name: "name", Value: fmt.Sprintf("%d", i+1)}) } h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Query("incTotal", "true"). Query("sort", "name DESC"). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). Assert(jsonpath.Equal(`$.response.filter.total`, float64(7))). End() } func TestRecordListWithPaginationAndSorting(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") helpers.AllowMe(h, module.RbacResource(), "records.search") var aux = struct { Response struct { Filter struct { NextPage *string PrevPage *string PageNavigation []struct { Page int Items int Cursor *string } } } }{} for i := 0; i < 7; i++ { h.makeRecord(module, &types.RecordValue{Name: "name", Value: fmt.Sprintf("%d", i+1)}) } // 1st page h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Query("incTotal", "true"). Query("incPageNavigation", "true"). Query("limit", "2"). Query("sort", "name DESC"). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). Assert(jsonpath.Equal(`$.response.set[0].values[0].value`, "7")). Assert(jsonpath.Equal(`$.response.set[1].values[0].value`, "6")). Assert(jsonpath.Equal(`$.response.filter.total`, float64(7))). Assert(jsonpath.Present(`$.response.filter.pageNavigation`)). Assert(jsonpath.Len(`$.response.filter.pageNavigation`, 4)). End(). JSON(&aux) h.a.Len(aux.Response.Filter.PageNavigation, 4) h.a.NotNil(aux.Response.Filter.PageNavigation[1].Cursor) // 2nd page h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Query("incTotal", "false"). Query("incPageNavigation", "false"). Query("limit", "2"). Query("pageCursor", *aux.Response.Filter.PageNavigation[1].Cursor). Query("sort", "name DESC"). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). Assert(jsonpath.Equal(`$.response.set[0].values[0].value`, "5")). Assert(jsonpath.Equal(`$.response.set[1].values[0].value`, "4")). Assert(jsonpath.NotPresent(`$.response.filter.total`)). Assert(jsonpath.NotPresent(`$.response.filter.pageNavigation`)). End() } func TestRecordListForbiddenRecords(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "records.search") helpers.DenyMe(h, types.RecordRbacResource(0, 0, 0), "read") h.makeRecord(module) h.makeRecord(module) h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Query("incTotal", "true"). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). // not present because omitted when empty Assert(jsonpath.NotPresent(`$.response.filter.total`)). End() } func TestRecordListForbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") helpers.AllowMe(h, module.RbacResource(), "records.create", "records.search") helpers.DenyMe(h, types.ModuleFieldRbacResource(0, 0, module.Fields[0].ID), "record.value.read") h.makeRecord(module, &types.RecordValue{Name: "name", Value: "v_name_0"}, &types.RecordValue{Name: "email", Value: "v_email_0"}) h.makeRecord(module, &types.RecordValue{Name: "name", Value: "v_name_1"}, &types.RecordValue{Name: "email", Value: "v_email_1"}) h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Query("incTotal", "true"). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). Assert(jsonpath.Equal(`$.response.filter.total`, float64(2))). Assert(jsonpath.Len(`$.response.set`, 2)). // rec 1 Assert(jsonpath.Len(`$.response.set[0].values`, 1)). Assert(jsonpath.Equal(`$.response.set[0].values[0].name`, "email")). // rec 2 Assert(jsonpath.Len(`$.response.set[1].values`, 1)). Assert(jsonpath.Equal(`$.response.set[1].values[0].name`, "email")). End() } func TestRecordCreateForbidden(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Header("Accept", "application/json"). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "val"}]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("record.errors.notAllowedToCreate")). End() } func TestRecordCreate(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") 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/", module.NamespaceID, module.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "val"}]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() } func TestRecordCreateForbidden_forbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module", &types.ModuleField{Name: "f1", Kind: "String"}, &types.ModuleField{Name: "f2", Kind: "String"}, ) helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") helpers.DenyMe(h, module.Fields[1].RbacResource(), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "f1", "value": "f1.v1"}, {"name": "f2", "value": "f2.v1"}]}`)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertErrorP("1 issue(s) found")). End() } func TestRecordCreate_forbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module", &types.ModuleField{Name: "f1", Kind: "String"}, &types.ModuleField{Name: "f2", Kind: "String"}, ) helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") helpers.DenyMe(h, module.Fields[1].RbacResource(), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "f1", "value": "f1.v1"}]}`)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() } func TestRecordCreate_xss(t *testing.T) { h := newHelper(t) h.clearRecords() var ( ns = h.makeNamespace("some-namespace") mod = h.makeModule(ns, "some-module", &types.ModuleField{ Kind: "String", Name: "dummy", }, &types.ModuleField{ Kind: "String", Name: "dummyRichTextBox", Options: map[string]interface{}{ "useRichTextEditor": true, }, }, ) ) helpers.AllowMe(h, ns.RbacResource(), "read") helpers.AllowMe(h, mod.RbacResource(), "read") helpers.AllowMe(h, mod.RbacResource(), "record.create") helpers.AllowMe(h, mod.Fields[0].RbacResource(), "record.value.update") helpers.AllowMe(h, mod.Fields[1].RbacResource(), "record.value.update") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") t.Run("create with rich text fields", func(t *testing.T) { var ( req = require.New(t) payload = struct { Response *types.Record }{} rec = &types.Record{ Values: types.RecordValueSet{ &types.RecordValue{Name: "dummyRichTextBox", Value: "test"}, &types.RecordValue{Name: "dummy", Value: "simple-text"}, }, } ) h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", ns.ID, mod.ID)). JSON(helpers.JSON(rec)). Expect(t). Status(http.StatusOK). Assert(jsonpath.Present(`$.response.values[? @.name=="dummyRichTextBox"]`)). Assert(jsonpath.Present(`$.response.values[? @.name=="dummy"]`)). Assert(jsonpath.Present(`$.response.values[? @.value=="simple-text"]`)). Assert(jsonpath.Present(`$.response.values[? @.value=="test"]`)). End(). JSON(&payload) req.NotNil(payload.Response) req.NotZero(payload.Response.ID) }) } func TestRecordCreateWithErrors(t *testing.T) { h := newHelper(t) h.clearRecords() fields := types.ModuleFieldSet{ &types.ModuleField{ ID: id.Next(), Name: "name", }, &types.ModuleField{ ID: id.Next(), Name: "required", Required: true, }, } module := h.repoMakeRecordModuleWithFields("record testing module", fields...) 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/", module.NamespaceID, module.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "val"}]}`)). Header("Accept", "application/json"). Expect(t). Assert(helpers.AssertRecordValueError( &types.RecordValueError{ Kind: "empty", Message: "record-field.errors.empty", Meta: map[string]interface{}{"field": "required"}, }, )). End() } func TestRecordUpdateForbidden(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module) 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": "changed-val"}]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("record.errors.notAllowedToUpdate")). End() } func TestRecordUpdate(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module) helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "changed-val"}]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() r := h.lookupRecordByID(module, record.ID) h.a.NotNil(r) } func TestRecordUpdate_missingField(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module", &types.ModuleField{Name: "f1", Kind: "String"}, &types.ModuleField{Name: "f2", Kind: "String"}, ) record := h.makeRecord(module, &types.RecordValue{Name: "f1", Value: "f1.v1"}, &types.RecordValue{Name: "f2", Value: "f2.v1"}, ) _ = record helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "update") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") api := h.apiInit() // delete f2 api. Post(fmt.Sprintf("/namespace/%d/module/%d", module.NamespaceID, module.ID)). JSON(fmt.Sprintf(`{"name": "%s", "handle": "%s", "fields": [{ "name": "f1", "kind": "String" }]}`, module.Name, module.Handle)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() // update f1 api. Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "f1", "value": "f1.v1 (edited)"}]}`)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() r := h.lookupRecordByID(module, record.ID) h.a.NotNil(r) h.a.Len(r.Values, 1) h.a.Equal("f1", r.Values[0].Name) h.a.Equal("f1.v1 (edited)", r.Values[0].Value) } func TestRecordUpdateForbidden_forbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module", &types.ModuleField{Name: "f1", Kind: "String"}, &types.ModuleField{Name: "f2", Kind: "String"}, ) record := h.makeRecord(module, &types.RecordValue{Name: "f1", Value: "f1.v1"}, &types.RecordValue{Name: "f2", Value: "f2.v1"}, ) helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") helpers.DenyMe(h, module.Fields[1].RbacResource(), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "f1", "value": "f1.v1"}, {"name": "f2", "value": "f2.v1 (edited)"}]}`)). Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("1 issue(s) found")). End() r := h.lookupRecordByID(module, record.ID) h.a.NotNil(r) h.a.Equal("f2.v1", r.Values.FilterByName("f2")[0].Value) } func TestRecordUpdate_forbiddenFields(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module", &types.ModuleField{Name: "f1", Kind: "String"}, &types.ModuleField{Name: "f2", Kind: "String"}, // we'll test all kinds of boolean inputs &types.ModuleField{Name: "f-b-f-n", Kind: "Bool"}, &types.ModuleField{Name: "f-b-f-m", Kind: "Bool"}, &types.ModuleField{Name: "f-b-f-e", Kind: "Bool"}, &types.ModuleField{Name: "f-b-f-z", Kind: "Bool"}, &types.ModuleField{Name: "f-b-t-n", Kind: "Bool"}, &types.ModuleField{Name: "f-b-t-m", Kind: "Bool"}, &types.ModuleField{Name: "f-b-t-v", Kind: "Bool"}, ) record := h.makeRecord(module, &types.RecordValue{Name: "f1", Value: "f1.v1"}, &types.RecordValue{Name: "f2", Value: "f2.v1"}, &types.RecordValue{Name: "f-b-f-n", Value: "0"}, // no-value &types.RecordValue{Name: "f-b-f-e", Value: "0"}, // empty &types.RecordValue{Name: "f-b-f-z", Value: "0"}, // zero &types.RecordValue{Name: "f-b-t-n", Value: "1"}, // no-value &types.RecordValue{Name: "f-b-t-v", Value: "1"}, // value ) helpers.AllowMe(h, types.RecordRbacResource(0, 0, record.ID), "update") helpers.AllowMe(h, module.Fields[0].RbacResource(), "record.value.update") helpers.DenyMe(h, types.ModuleFieldRbacResource(0, record.ModuleID, 0), "record.value.update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [ {"name": "f1", "value": "f1.v1"}, {"name": "f2", "value": "f2.v1"}, {"name": "f-b-f-n"}, {"name": "f-b-f-e", "value": ""}, {"name": "f-b-f-z", "value": "0"}, {"name": "f-b-t-v", "value": "1"} ]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() r := h.lookupRecordByID(module, record.ID) h.a.NotNil(r) h.a.Equal("f2.v1", r.Values.FilterByName("f2")[0].Value) h.a.Equal("", r.Values.FilterByName("f-b-f-n")[0].Value) h.a.Equal("", r.Values.FilterByName("f-b-f-e")[0].Value) h.a.Equal("", r.Values.FilterByName("f-b-f-z")[0].Value) h.a.Equal("1", r.Values.FilterByName("f-b-t-n")[0].Value) h.a.Equal("1", r.Values.FilterByName("f-b-t-v")[0].Value) } func TestRecordUpdate_refUnchanged(t *testing.T) { h := newHelper(t) h.clearRecords() namespace := h.makeNamespace("record testing namespace") // mods mRef := h.makeRecordModuleWithFieldsOnNs("record testing module", namespace) module := h.makeRecordModuleWithFieldsOnNs("record testing module", namespace, &types.ModuleField{ Name: "name", Kind: "String", }, &types.ModuleField{ Name: "ref", Kind: "Record", Options: types.ModuleFieldOptions{ "moduleID": strconv.FormatUint(mRef.ID, 10), }, }, ) // Records rRef := h.makeRecord(mRef) record := h.makeRecord(module, &types.RecordValue{ Name: "name", Value: "value; name", }, &types.RecordValue{ Name: "ref", Value: strconv.FormatUint(rRef.ID, 10), Ref: rRef.ID, }, ) helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "changed-val"}, {"name": "ref", "value": "%d"}]}`, rRef.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() r := h.lookupRecordByID(module, record.ID) h.a.Equal(rRef.ID, r.Values.Get("ref", 0).Ref) h.a.Equal(strconv.FormatUint(rRef.ID, 10), r.Values.Get("ref", 0).Value) h.a.NotNil(r) } func TestRecordUpdate_refChanged(t *testing.T) { h := newHelper(t) h.clearRecords() namespace := h.makeNamespace("record testing namespace") // mods mRef := h.makeRecordModuleWithFieldsOnNs("record testing module (ref)", namespace) module := h.makeRecordModuleWithFieldsOnNs("record testing module", namespace, &types.ModuleField{ Name: "name", Kind: "String", }, &types.ModuleField{ Name: "ref", Kind: "Record", Options: types.ModuleFieldOptions{ "moduleID": strconv.FormatUint(mRef.ID, 10), }, }, ) // Records rRef := h.makeRecord(mRef) rRef2 := h.makeRecord(mRef) record := h.makeRecord(module, &types.RecordValue{ Name: "name", Value: "value; name", }, &types.RecordValue{ Name: "ref", Value: strconv.FormatUint(rRef.ID, 10), Ref: rRef.ID, }, ) helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") h.apiInit(). Debug(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "changed-val"}, {"name": "ref", "value": "%d"}]}`, rRef2.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() r := h.lookupRecordByID(module, record.ID) h.a.Equal(rRef2.ID, r.Values.Get("ref", 0).Ref) h.a.Equal(strconv.FormatUint(rRef2.ID, 10), r.Values.Get("ref", 0).Value) h.a.NotNil(r) } func TestRecordUpdate_deleteOld(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module, &types.RecordValue{Name: "name", Value: "test name"}, &types.RecordValue{Name: "email", Value: "test@email.tld"}) helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). JSON(fmt.Sprintf(`{"values": [{"name": "email", "value": "test@email.tld"}]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). Assert(jsonpath.Len(`$.response.values`, 1)). End() r := h.lookupRecordByID(module, record.ID) h.a.NotNil(r) h.a.Empty(r.Values.FilterByName("name")) } func TestRecordDeleteForbidden(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module) h.apiInit(). Delete(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertError("record.errors.notAllowedToDelete")). End() } func TestRecordDelete(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module") record := h.makeRecord(module) helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "delete") h.apiInit(). Delete(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() r := h.lookupRecordByID(module, record.ID) h.a.NotNil(r.DeletedAt) } func TestRecordAttachment(t *testing.T) { h := newHelper(t) h.clearRecords() namespace := h.makeNamespace("record attachment testing namespace") helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read", "record.create") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update") const maxSizeLimit = 1 module := h.makeModule( namespace, "module", &types.ModuleField{Name: "no_constraints", Kind: "File"}, &types.ModuleField{ Name: "max_size", Kind: "File", Options: types.ModuleFieldOptions{"maxSize": maxSizeLimit}, }, &types.ModuleField{ Name: "img_only", Kind: "File", Options: types.ModuleFieldOptions{ "maxSize": maxSizeLimit, "mimetypes": "image/gif, image/png, image/jpeg", }, }, &types.ModuleField{Name: "str", Kind: "String"}, &types.ModuleField{ Name: "csv_only", Kind: "File", Options: types.ModuleFieldOptions{ "mimetypes": "text/csv", }, }, ) xxlBlob := bytes.Repeat([]byte("0"), maxSizeLimit*1_000_000+1) testImgFh, err := os.ReadFile("./testdata/test.png") h.noError(err) testCsvFh, err := os.ReadFile("./testdata/test.csv") h.noError(err) defer func() { // reset settings after we're done systemService.CurrentSettings.Compose.Record.Attachments.MaxSize = 0 systemService.CurrentSettings.Compose.Record.Attachments.Mimetypes = nil }() systemService.CurrentSettings.Compose.Record.Attachments.MaxSize = maxSizeLimit systemService.CurrentSettings.Compose.Record.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{"fieldName": "no_constraints"}, helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"), }, { "no file", nil, "empty", "plain/text", map[string]string{"fieldName": "no_constraints"}, helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"), }, { "no field", []byte("."), "dot", "plain/text", nil, helpers.AssertError("attachment.errors.invalidModuleField"), }, { "invalid field", []byte("."), "dot", "plain/text", map[string]string{"fieldName": "str"}, helpers.AssertError("attachment.errors.invalidModuleField"), }, { "valid upload, no constraints", []byte("."), "dot", "plain/text", map[string]string{"fieldName": "no_constraints"}, helpers.AssertNoErrors, }, { "global max size - over sized", xxlBlob, "numbers", "plain/text", map[string]string{"fieldName": "no_constraints"}, helpers.AssertError("attachment.errors.tooLarge"), }, { "field max size - ok", []byte("12345"), "numbers", "plain/text", map[string]string{"fieldName": "max_size"}, helpers.AssertNoErrors, }, { "field max size - over sized", xxlBlob, "numbers", "plain/text", map[string]string{"fieldName": "max_size"}, helpers.AssertError("attachment.errors.tooLarge"), }, { "global mimetype - invalid", testImgFh, "numbers.gif", "image/gif", map[string]string{"fieldName": "no_constraints"}, helpers.AssertError("attachment.errors.failedToProcessImage"), }, { "field mimetype - ok", testImgFh, "image.png", "image/gif", map[string]string{"fieldName": "img_only"}, helpers.AssertNoErrors, }, { "field mimetype - invalid", testImgFh, "image.png", "image/gif", map[string]string{"fieldName": "img_only"}, helpers.AssertNoErrors, }, { "csv file - ok", testCsvFh, "testCSV", "text/csv", map[string]string{"fieldName": "csv_only"}, helpers.AssertNoErrors, }, } for _, c := range cc { t.Run(c.name, func(t *testing.T) { h.t = t helpers.InitFileUpload(t, h.apiInit(), fmt.Sprintf("/namespace/%d/module/%d/record/attachment", module.NamespaceID, module.ID), c.form, c.file, c.fname, c.mtype, ). Status(http.StatusOK). Assert(c.test). End() }) } } func TestRecordExport(t *testing.T) { t.Skip("@todo not yet refactored") // h := newHelper(t) // h.clearRecords() // module := h.repoMakeRecordModuleWithFields("record export module") // expected := "id,name\n" // for i := 0; i < 10; i++ { // r := h.makeRecord(module, &types.RecordValue{Name: "name", Value: fmt.Sprintf("d%d", i), Place: uint(i)}) // expected += fmt.Sprintf("%d,d%d\n", r.ID, i) // } // // we'll not use standard asserts (AssertNoErrors) here, // // because we're not returning JSON errors. // r := h.apiInit(). // Get(fmt.Sprintf("/namespace/%d/module/%d/record/export.csv", module.NamespaceID, module.ID)). // Query("fields", "name"). // Header("Accept", "application/json"). // Expect(t). // Status(http.StatusOK). // End() // b, err := ioutil.ReadAll(r.Response.Body) // h.noError(err) // h.a.Equal(expected, string(b)) } func (h helper) apiInitRecordImport(api *apitest.APITest, url, f string, file []byte) *apitest.Response { body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("upload", f) h.noError(err) _, err = part.Write(file) h.noError(err) h.noError(writer.Close()) return api. Post(url). Header("Accept", "application/json"). Body(body.String()). ContentType(writer.FormDataContentType()). Expect(h.t). Status(http.StatusOK) } func (h helper) apiRunRecordImport(api *apitest.APITest, url, b string) *apitest.Response { return api. Patch(url). Header("Accept", "application/json"). JSON(b). Expect(h.t). Status(http.StatusOK) } func TestRecordImportInit(t *testing.T) { t.Skip("@todo not yet refactored") // h := newHelper(t) // h.clearRecords() // module := h.repoMakeRecordModuleWithFields("record import init module") // tests := []struct { // Name string // Content string // }{ // { // Name: "f1.csv", // Content: "name,email\nv1,v2\n", // }, // { // Name: "f1.json", // Content: `{"name":"v1","email":"v2"}` + "\n", // }, // } // for _, test := range tests { // t.Run(test.Name, func(t *testing.T) { // url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) // h.apiInitRecordImport(h.apiInit(), url, test.Name, []byte(test.Content)). // Assert(jsonpath.Present("$.response.sessionID")). // Assert(jsonpath.Present(`$.response.fields.name==""`)). // Assert(jsonpath.Present(`$.response.fields.email==""`)). // Assert(jsonpath.Present("$.response.progress")). // Assert(jsonpath.Present("$.response.progress.entryCount==1")). // End() // }) // } } func TestRecordImportInit_invalidFileFormat(t *testing.T) { t.Skip("@todo not yet refactored") // h := newHelper(t) // h.clearRecords() // module := h.repoMakeRecordModuleWithFields("record import init module") // url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) // h.apiInitRecordImport(h.apiInit(), url, "invalid", []byte("nope")). // Assert(helpers.AssertError("compose.service.RecordImportFormatNotSupported")). // End() // } // func TestRecordImportRun(t *testing.T) { // h := newHelper(t) // h.clearRecords() // helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") // module := h.repoMakeRecordModuleWithFields("record import run module") // tests := []struct { // Name string // Content string // }{ // { // Name: "f1.csv", // Content: "fname,femail\nv1,v2\n", // }, // } // for _, test := range tests { // t.Run(test.Name, func(t *testing.T) { // url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) // rsp := &rImportSession{} // api := h.apiInit() // r := h.apiInitRecordImport(api, url, test.Name, []byte(test.Content)).End() // r.JSON(rsp) // h.apiRunRecordImport(api, fmt.Sprintf("%s/%s", url, rsp.Response.SessionID), `{"fields":{"fname":"name","femail":"email"},"onError":"fail"}`). // Assert(helpers.AssertNoErrors). // Assert(jsonpath.Present("$.response.progress")). // Assert(jsonpath.Present(`$.response.fields.fname=="name"`)). // Assert(jsonpath.Present(`$.response.fields.femail=="email"`)). // End() // }) // } } func TestRecordImportRun_sessionNotFound(t *testing.T) { t.Skip("@todo not yet refactored") // h := newHelper(t) // h.clearRecords() // module := h.repoMakeRecordModuleWithFields("record import run module") // h.apiRunRecordImport(h.apiInit(), fmt.Sprintf("/namespace/%d/module/%d/record/import/123", module.NamespaceID, module.ID), `{"fields":{"fname":"name","femail":"email"},"onError":"fail"}`). // Assert(helpers.AssertError("compose.service.RecordImportSessionNotFound")). // End() } // @todo revert whe we add import RBAC operations // func TestRecordImportRunForbidden(t *testing.T) { // h := newHelper(t) // h.clearRecords() // helpers.DenyMe(h, types.ModuleRbacResource(0, 0), "record.create") // module := h.repoMakeRecordModuleWithFields("record import run module") // tests := []struct { // Name string // Content string // }{ // { // Name: "f1.csv", // Content: "fname,femail\nv1,v2\n", // }, // } // for _, test := range tests { // t.Run(test.Name, func(t *testing.T) { // url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) // rsp := &rImportSession{} // api := h.apiInit() // r := h.apiInitRecordImport(api, url, test.Name, []byte(test.Content)).End() // r.JSON(rsp) // h.apiRunRecordImport(api, fmt.Sprintf("%s/%s", url, rsp.Response.SessionID), `{"fields":{"fname":"name","femail":"email"},"onError":"fail"}`). // Assert(helpers.AssertErrorP("record.errors.notAllowedToCreate for module")). // End() // }) // } // } // @todo revert whe we add import RBAC operations // func TestRecordImportRunForbidden_field(t *testing.T) { // h := newHelper(t) // h.clearRecords() // helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") // helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") // module := h.repoMakeRecordModuleWithFields("record import run module") // f := module.Fields.FindByName("name") // helpers.DenyMe(h, f.RbacResource(), "record.value.update") // tests := []struct { // Name string // Content string // }{ // { // Name: "f1.csv", // Content: "fname,femail\nv1,v2\n", // }, // } // for _, test := range tests { // t.Run(test.Name, func(t *testing.T) { // h.t = t // url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) // rsp := &rImportSession{} // api := h.apiInit() // r := h.apiInitRecordImport(api, url, test.Name, []byte(test.Content)).End() // r.JSON(rsp) // h.apiRunRecordImport(api, fmt.Sprintf("%s/%s", url, rsp.Response.SessionID), `{"fields":{"fname":"name","femail":"email"},"onError":"fail"}`). // Assert(helpers.AssertErrorP("1 issue(s) found")). // End() // }) // } // } func TestRecordImportRunFieldError_missing(t *testing.T) { t.Skip("@todo not yet refactored") // h := newHelper(t) // h.clearRecords() // helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") // module := h.repoMakeRecordModuleWithFieldsRequired("record import run module") // tests := []struct { // Name string // Content string // }{ // { // Name: "f1.csv", // Content: "fname,femail\n,v2\n", // }, // } // for _, test := range tests { // t.Run(test.Name, func(t *testing.T) { // url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) // rsp := &rImportSession{} // api := h.apiInit() // r := h.apiInitRecordImport(api, url, test.Name, []byte(test.Content)).End() // r.JSON(rsp) // h.apiRunRecordImport(api, fmt.Sprintf("%s/%s", url, rsp.Response.SessionID), `{"fields":{"femail":"email"},"onError":"skip"}`). // End() // api.Get(fmt.Sprintf("%s/%s", url, rsp.Response.SessionID)). // Expect(h.t). // Status(http.StatusOK). // Assert(helpers.AssertNoErrors). // Assert(jsonpath.Present("$.response.progress.failLog.errors[\"empty field name\"]")). // End() // }) // } } func TestRecordImportImportProgress(t *testing.T) { t.Skip("@todo not yet refactored") // h := newHelper(t) // h.clearRecords() // module := h.repoMakeRecordModuleWithFields("record import session module") // tests := []struct { // Name string // Content string // }{ // { // Name: "f1.csv", // Content: "fname,femail\nv1,v2\n", // }, // } // for _, test := range tests { // t.Run(test.Name, func(t *testing.T) { // url := fmt.Sprintf("/namespace/%d/module/%d/record/import", module.NamespaceID, module.ID) // rsp := &rImportSession{} // api := h.apiInit() // r := h.apiInitRecordImport(api, url, test.Name, []byte(test.Content)).End() // r.JSON(rsp) // api.Get(fmt.Sprintf("%s/%s", url, rsp.Response.SessionID)). // Expect(h.t). // Status(http.StatusOK). // Assert(helpers.AssertNoErrors). // Assert(jsonpath.Present("$.response.progress")). // End() // }) // } } func TestRecordImportImportProgress_sessionNotFound(t *testing.T) { t.Skip("@todo not yet refactored") // h := newHelper(t) // h.clearRecords() // module := h.repoMakeRecordModuleWithFields("record import module") // h.apiInit(). // Get(fmt.Sprintf("/namespace/%d/module/%d/record/import/123", module.NamespaceID, module.ID)). // Header("Accept", "application/json"). // Expect(h.t). // Status(http.StatusOK). // Assert(helpers.AssertError("compose.service.RecordImportSessionNotFound")). // End() } func TestRecordFieldModulePermissionCheck(t *testing.T) { h := newHelper(t) h.clearRecords() // make a standard module, and prevent (DENY) current user to // read from "name" and update "email" fields module := h.repoMakeRecordModuleWithFields("record testing module") helpers.AllowMe(h, module.RbacResource(), "records.create", "records.search") helpers.DenyMe(h, module.Fields.FindByName("name").RbacResource(), "record.value.read") helpers.DenyMe(h, module.Fields.FindByName("email").RbacResource(), "record.value.update") helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update") record := h.makeRecord( module, &types.RecordValue{Name: "name", Value: "should not be readable"}, &types.RecordValue{Name: "email", Value: "should not be writable"}, ) // Fetching record should work as before but without read-protected fields h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). // should not return name Assert(jsonpath.NotPresent(`$.response.values[? @.name=="name"]`)). // should return email Assert(jsonpath.Present(`$.response.values[? @.name=="email"]`)). End() // Searching records should work as before but without read-protected fields h.apiInit(). Get(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). // should not return name Assert(jsonpath.NotPresent(`$.response.set[0].values[? @.name=="name"]`)). // should return email Assert(jsonpath.Present(`$.response.set[0].values[? @.name=="email"]`)). End() bb := map[string]func() *apitest.Request{ "update": func() *apitest.Request { return h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", module.NamespaceID, module.ID, record.ID)) }, "create": func() *apitest.Request { return h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", module.NamespaceID, module.ID)) }, } for name, b := range bb { t.Run(name, func(t *testing.T) { t.Run("field:email", func(t *testing.T) { // Try to change email (not writable!), expect error... b().JSON(fmt.Sprintf(`{"values": [{"name": "email", "value": "changed-email"}]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertErrorP("1 issue(s) found")). End() }) t.Run("field:name", func(t *testing.T) { // Try to change name, (not readable), expect it to work b().JSON(fmt.Sprintf(`{"values": [{"name": "name", "value": "changed-name"}]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() }) t.Run("field:description", func(t *testing.T) { // Try to change description, (no perm. rules), expect it to work b().JSON(fmt.Sprintf(`{"values": [{"name": "description", "value": "changed-description"}]}`)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End() }) }) } } func TestRecordLabels(t *testing.T) { h := newHelper(t) h.clearRecords() helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read", "record.create", "records.search") helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "update", "read") var ( ns = h.makeNamespace("some-namespace") mod = h.makeModule(ns, "some-module", &types.ModuleField{Kind: "String", Name: "dummy"}) ID uint64 ) helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "record.create", "read") helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read") helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.update") t.Run("create", func(t *testing.T) { var ( req = require.New(t) payload = struct { Response *types.Record }{} rec = &types.Record{ Values: types.RecordValueSet{&types.RecordValue{Name: "dummy", Value: "dummy"}}, Meta: map[string]any{ "foo": "bar", "bar": "42", }, } ) h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/", ns.ID, mod.ID)). JSON(helpers.JSON(rec)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End(). JSON(&payload) req.NotNil(payload.Response) req.NotZero(payload.Response.ID) h.a.Equal(payload.Response.Meta["foo"], "bar", "labels must contain foo with value bar") h.a.Equal(payload.Response.Meta["bar"], "42", "labels must contain bar with value 42") ID = payload.Response.ID }) t.Run("update", func(t *testing.T) { if ID == 0 { t.Skip("label/create test not ran") } var ( req = require.New(t) payload = struct { Response *types.Record }{} rec = &types.Record{ ID: ID, Values: types.RecordValueSet{&types.RecordValue{Name: "dummy", Value: "dummy"}}, Meta: map[string]any{ "foo": "baz", "baz": "123", }, } ) h.apiInit(). Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", ns.ID, mod.ID, ID)). JSON(helpers.JSON(rec)). Header("Accept", "application/json"). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End(). JSON(&payload) req.NotZero(payload.Response.ID) // disabled for now //req.Nil(payload.Response.UpdatedAt, "updatedAt must not change after changing labels") req.Equal(payload.Response.Meta["foo"], "baz", "meta must contain foo with value baz") req.NotContains(payload.Response.Meta, "bar", "meta must not contain bar") req.Equal(payload.Response.Meta["baz"], "123", "meta must contain baz with value 123") }) t.Run("search", func(t *testing.T) { if ID == 0 { t.Skip("label/create test not ran") } var ( req = require.New(t) payload = struct { Response struct { Set types.RecordSet } }{} get = func(meta string, p any) { h.apiInit(). Getf("/namespace/%d/module/%d/record/", ns.ID, mod.ID). Header("Accept", "application/json"). Query("meta", meta). Expect(t). Status(http.StatusOK). Assert(helpers.AssertNoErrors). End(). JSON(p) } ) get("baz=123", &payload) t.Log("is record included in search result?") req.NotEmpty(payload.Response.Set) req.Len(payload.Response.Set, 1) req.NotNil(payload.Response.Set.FindByID(ID)) t.Log("is meta included") req.NotNil(payload.Response.Set.FindByID(ID).Meta) get("k2342341241241244=bar", &payload) t.Log("no records with this meta constraints should exist") req.Empty(payload.Response.Set) get("baz", &payload) t.Log("one record should be found") req.Len(payload.Response.Set, 1) req.NotNil(payload.Response.Set.FindByID(ID)) }) } func TestRecordReports(t *testing.T) { t.Skip("@todo not yet refactored") // h := newHelper(t) // h.clearRecords() // helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "records.search") // helpers.AllowMe(h, types.NamespaceRbacResource(0), "read") // helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read", "record.create") // helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read") // var ( // ns = h.makeNamespace("some-namespace") // mod = h.makeModule(ns, "some-module", // &types.ModuleField{ // Kind: "Number", // Name: "n_float", // Options: types.ModuleFieldOptions{"precision": 2}, // }, // &types.ModuleField{ // Kind: "Number", // Name: "n_int", // Options: types.ModuleFieldOptions{"precision": 0}, // }, // &types.ModuleField{ // Kind: "Number", // Name: "n_int_multi", // Multi: true, // Options: types.ModuleFieldOptions{"precision": 0}, // }, // ) // ) // h.makeRecord(mod, // &types.RecordValue{Name: "n_float", Value: "1.1"}, // &types.RecordValue{Name: "n_int", Value: "1"}, // &types.RecordValue{Name: "n_int_multi", Value: "1"}, // ) // h.makeRecord(mod, // &types.RecordValue{Name: "n_float", Value: "2.3"}, // &types.RecordValue{Name: "n_int", Value: "2"}, // &types.RecordValue{Name: "n_int_multi", Value: "1"}, // &types.RecordValue{Name: "n_int_multi", Value: "2", Place: 1}, // &types.RecordValue{Name: "n_int_multi", Value: "3", Place: 2}, // ) // t.Run("base metrics", func(t *testing.T) { // tcc := []struct { // op string // expCount float64 // expFloat float64 // expInteger float64 // expMultInt float64 // }{ // { // op: "COUNT", // expCount: 2, // expFloat: 2, // expInteger: 2, // expMultInt: 4, // counting multi values as well // }, // { // op: "SUM", // expCount: 2, // expFloat: 3.4, // expInteger: 3, // expMultInt: 7, // summing multi values as well // }, // { // op: "MAX", // expCount: 2, // expFloat: 2.3, // expInteger: 2, // expMultInt: 3, // all values, even the last one // }, // { // op: "MIN", // expCount: 2, // expFloat: 1.1, // expInteger: 1, // expMultInt: 1, // }, // { // op: "AVG", // expCount: 2, // expFloat: 1.7, // expInteger: 1.5, // expMultInt: 1.75, // all values! // }, // // @todo // // { // // op: "STD", // // expFloat: 0, // // expInteger: 0, // // }, // } // for _, tc := range tcc { // t.Run("basic operations; float; "+tc.op, func(t *testing.T) { // h.apiInit(). // Get(fmt.Sprintf("/namespace/%d/module/%d/record/report", mod.NamespaceID, mod.ID)). // Query("metrics", tc.op+"(n_float) as rp"). // Query("dimensions", "DATE_FORMAT(created_at,'Y-01-01')"). // Header("Accept", "application/json"). // Expect(t). // Status(http.StatusOK). // Assert(jsonpath.Len(`$.response`, 1)). // Assert(jsonpath.Equal(`$.response[0].count`, tc.expCount)). // Assert(jsonpath.Equal(`$.response[0].rp`, tc.expFloat)). // End() // }) // t.Run("basic operations; int; "+tc.op, func(t *testing.T) { // h.apiInit(). // Get(fmt.Sprintf("/namespace/%d/module/%d/record/report", mod.NamespaceID, mod.ID)). // Query("metrics", tc.op+"(n_int) as rp"). // Query("dimensions", "DATE_FORMAT(created_at,'Y-01-01')"). // Header("Accept", "application/json"). // Expect(t). // Status(http.StatusOK). // Assert(jsonpath.Len(`$.response`, 1)). // Assert(jsonpath.Equal(`$.response[0].count`, tc.expCount)). // Assert(jsonpath.Equal(`$.response[0].rp`, tc.expInteger)). // End() // }) // t.Run("basic operations; int multi-value-field; "+tc.op, func(t *testing.T) { // h.apiInit(). // Get(fmt.Sprintf("/namespace/%d/module/%d/record/report", mod.NamespaceID, mod.ID)). // Query("metrics", tc.op+"(n_int_multi) as rp"). // Query("dimensions", "DATE_FORMAT(created_at,'Y-01-01')"). // Header("Accept", "application/json"). // Expect(t). // Status(http.StatusOK). // Assert(jsonpath.Len(`$.response`, 1)). // Assert(jsonpath.Equal(`$.response[0].count`, tc.expCount)). // Assert(jsonpath.Equal(`$.response[0].rp`, tc.expMultInt)). // End() // }) // } // }) } func TestRecordClone(t *testing.T) { h := newHelper(t) h.clearRecords() module := h.repoMakeRecordModuleWithFields("record testing module", &types.ModuleField{Name: "name", Kind: "String"}, &types.ModuleField{Name: "description", Kind: "String"}, ) // Create original record original := h.makeRecord(module, &types.RecordValue{Name: "name", Value: "Original Name"}, &types.RecordValue{Name: "description", Value: "Original Description"}, ) // Clone the record cloned := original.Clone() // Verify cloned record h.a.NotNil(cloned) h.a.Equal(original.ID, cloned.ID) h.a.Equal(original.ModuleID, cloned.ModuleID) h.a.Equal(original.NamespaceID, cloned.NamespaceID) h.a.Equal(original.Values.Get("name", 0).Value, cloned.Values.Get("name", 0).Value) h.a.Equal(original.Values.Get("description", 0).Value, cloned.Values.Get("description", 0).Value) h.a.Equal(original.CreatedBy, cloned.CreatedBy) h.a.Equal(original.UpdatedBy, cloned.UpdatedBy) h.a.Equal(original.DeletedBy, cloned.DeletedBy) h.a.Equal(original.CreatedAt, cloned.CreatedAt) h.a.Equal(original.UpdatedAt, cloned.UpdatedAt) h.a.Equal(original.DeletedAt, cloned.DeletedAt) // Modify cloned record values cloned.Values = cloned.Values.Set(&types.RecordValue{Name: "name", Value: "Modified Name"}) cloned.Values = cloned.Values.Set(&types.RecordValue{Name: "description", Value: "Modified Description"}) // Verify original record is not affected h.a.Equal("Original Name", original.Values.Get("name", 0).Value, "Original record name should not be affected") h.a.Equal("Original Description", original.Values.Get("description", 0).Value, "Original record description should not be affected") // Verify cloned record has new values h.a.Equal("Modified Name", cloned.Values.Get("name", 0).Value, "Cloned record should have modified name") h.a.Equal("Modified Description", cloned.Values.Get("description", 0).Value, "Cloned record should have modified description") }