3
0

Add template provisioning

This commit is contained in:
Tomaž Jerman 2021-03-09 15:49:04 +01:00
parent c4995dd8eb
commit 1d5bedc2be
22 changed files with 783 additions and 19 deletions

View File

@ -0,0 +1,65 @@
package resource
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
// Template represents a Template
Template struct {
*base
Res *types.Template
}
)
func NewTemplate(t *types.Template) *Template {
r := &Template{base: &base{}}
r.SetResourceType(TEMPLATE_RESOURCE_TYPE)
r.Res = t
r.AddIdentifier(identifiers(t.Handle, t.Meta.Short, t.ID)...)
// Initial timestamps
r.SetTimestamps(MakeCUDASTimestamps(&t.CreatedAt, t.UpdatedAt, t.DeletedAt, nil, nil))
return r
}
func (r *Template) SysID() uint64 {
return r.Res.ID
}
func (r *Template) Ref() string {
return FirstOkString(r.Res.Handle, r.Res.Meta.Short, strconv.FormatUint(r.Res.ID, 10))
}
// FindTemplate looks for the template in the resources
func FindTemplate(rr InterfaceSet, ii Identifiers) (u *types.Template) {
var tRes *Template
rr.Walk(func(r Interface) error {
tr, ok := r.(*Template)
if !ok {
return nil
}
if tr.Identifiers().HasAny(ii) {
tRes = tr
}
return nil
})
// Found it
if tRes != nil {
return tRes.Res
}
return nil
}
func TemplateErrUnresolved(ii Identifiers) error {
return fmt.Errorf("template unresolved %v", ii.StringSlice())
}

View File

@ -49,6 +49,7 @@ var (
ROLE_RESOURCE_TYPE = st.RoleRBACResource.String()
SETTINGS_RESOURCE_TYPE = "system:setting:"
USER_RESOURCE_TYPE = st.UserRBACResource.String()
TEMPLATE_RESOURCE_TYPE = st.TemplateRBACResource.String()
DATA_SOURCE_RESOURCE_TYPE = "data:raw:"
)

View File

@ -22,6 +22,7 @@ type (
// System stuff
roles []*roleFilter
users []*userFilter
templates []*templateFilter
applications []*applicationFilter
settings []*settingFilter
rbac []*rbacFilter
@ -87,6 +88,7 @@ func (d *decoder) Decode(ctx context.Context, s store.Storer, f *DecodeFilter) (
system.decodeRoles(ctx, s, f.roles),
system.decodeUsers(ctx, s, f.users),
system.decodeTemplates(ctx, s, f.templates),
system.decodeApplications(ctx, s, f.applications),
system.decodeSettings(ctx, s, f.settings),
)

View File

@ -132,6 +132,8 @@ func (se *storeEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState)
// System resources
case *resource.User:
err = f(NewUserFromResource(res, se.cfg), ers)
case *resource.Template:
err = f(NewTemplateFromResource(res, se.cfg), ers)
case *resource.Role:
err = f(NewRoleFromResource(res, se.cfg), ers)
case *resource.Application:

View File

@ -12,6 +12,7 @@ import (
type (
roleFilter types.RoleFilter
userFilter types.UserFilter
templateFilter types.TemplateFilter
applicationFilter types.ApplicationFilter
settingFilter types.SettingsFilter
rbacFilter struct {
@ -28,6 +29,7 @@ type (
store.Roles
store.Settings
store.Users
store.Templates
}
systemDecoder struct {
@ -130,6 +132,52 @@ func (d *systemDecoder) decodeUsers(ctx context.Context, s systemStore, ff []*us
mm: mm,
}
}
func (d *systemDecoder) decodeTemplates(ctx context.Context, s systemStore, ff []*templateFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
return &auxRsp{
mm: mm,
}
}
var nn types.TemplateSet
var fn types.TemplateFilter
var err error
for _, f := range ff {
aux := *f
if aux.Limit == 0 {
aux.Limit = 1000
}
for {
nn, fn, err = s.SearchTemplates(ctx, types.TemplateFilter(aux))
if err != nil {
return &auxRsp{
err: err,
}
}
for _, n := range nn {
mm = append(mm, newTemplate(n))
d.resourceID = append(d.resourceID, n.ID)
}
if fn.NextPage != nil {
aux.PageCursor = fn.NextPage
} else {
break
}
}
}
return &auxRsp{
mm: mm,
}
}
func (d *systemDecoder) decodeApplications(ctx context.Context, s systemStore, ff []*applicationFilter) *auxRsp {
mm := make([]envoy.Marshaller, 0, 100)
if ff == nil {
@ -276,6 +324,15 @@ func (df *DecodeFilter) Users(f *types.UserFilter) *DecodeFilter {
return df
}
// Templates adds a new TemplateFilter
func (df *DecodeFilter) Templates(f *types.TemplateFilter) *DecodeFilter {
if df.templates == nil {
df.templates = make([]*templateFilter, 0, 1)
}
df.templates = append(df.templates, (*templateFilter)(f))
return df
}
// Applications adds a new ApplicationFilter
func (df *DecodeFilter) Applications(f *types.ApplicationFilter) *DecodeFilter {
if df.applications == nil {

View File

@ -0,0 +1,98 @@
package store
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
template struct {
cfg *EncoderConfig
res *resource.Template
t *types.Template
}
)
// mergeTemplates merges b into a, prioritising a
func mergeTemplates(a, b *types.Template) *types.Template {
c := *a
if c.Handle == "" {
c.Handle = b.Handle
}
if c.Language == "" {
c.Language = b.Language
}
if c.Type == "" {
c.Type = b.Type
}
if c.Meta.Short == "" {
c.Meta.Short = b.Meta.Short
}
if c.Meta.Description == "" {
c.Meta.Description = b.Meta.Description
}
if c.Template == "" {
c.Template = b.Template
}
if c.OwnerID == 0 {
c.OwnerID = b.OwnerID
}
if c.CreatedAt.IsZero() {
c.CreatedAt = b.CreatedAt
}
if c.UpdatedAt == nil {
c.UpdatedAt = b.UpdatedAt
}
if c.DeletedAt == nil {
c.DeletedAt = b.DeletedAt
}
return &c
}
// findTemplateRS looks for the template in the resources & the store
//
// Provided resources are prioritized.
func findTemplateRS(ctx context.Context, s store.Storer, rr resource.InterfaceSet, ii resource.Identifiers) (u *types.Template, err error) {
u = resource.FindTemplate(rr, ii)
if u != nil {
return u, nil
}
return findTemplateS(ctx, s, makeGenericFilter(ii))
}
// findTemplateS looks for the template in the store
func findTemplateS(ctx context.Context, s store.Storer, gf genericFilter) (t *types.Template, err error) {
if gf.id > 0 {
t, err = store.LookupTemplateByID(ctx, s, gf.id)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if t != nil {
return
}
}
for _, i := range gf.identifiers {
// Handle & templatename
t, err = store.LookupTemplateByHandle(ctx, s, i)
if err != nil && err != store.ErrNotFound {
return nil, err
}
if t != nil {
return
}
}
return nil, nil
}

View File

@ -0,0 +1,106 @@
package store
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/store"
)
func NewTemplateFromResource(res *resource.Template, cfg *EncoderConfig) resourceState {
return &template{
cfg: mergeConfig(cfg, res.Config()),
res: res,
}
}
// Prepare prepares the template to be encoded
//
// Any validation, additional constraining should be performed here.
func (n *template) Prepare(ctx context.Context, pl *payload) (err error) {
// Try to get the original template
n.t, err = findTemplateS(ctx, pl.s, makeGenericFilter(n.res.Identifiers()))
if err != nil {
return err
}
if n.t != nil {
n.res.Res.ID = n.t.ID
}
return nil
}
// Encode encodes the template to the store
//
// Encode is allowed to do some data manipulation, but no resource constraints
// should be changed.
func (n *template) Encode(ctx context.Context, pl *payload) (err error) {
res := n.res.Res
exists := n.t != nil && n.t.ID > 0
// Determine the ID
if res.ID <= 0 && exists {
res.ID = n.t.ID
}
if res.ID <= 0 {
res.ID = NextID()
}
// Timestamps
ts := n.res.Timestamps()
if ts != nil {
if ts.CreatedAt != nil {
res.CreatedAt = *ts.CreatedAt.T
} else {
res.CreatedAt = *now()
}
if ts.UpdatedAt != nil {
res.UpdatedAt = ts.UpdatedAt.T
}
if ts.DeletedAt != nil {
res.DeletedAt = ts.DeletedAt.T
}
}
// Userstamps
us := n.res.Userstamps()
if us != nil {
if us.OwnedBy != nil {
res.OwnerID = us.OwnedBy.UserID
}
}
// Evaluate the resource skip expression
// @todo expand available parameters; similar implementation to compose/types/record@Dict
if skip, err := basicSkipEval(ctx, n.cfg, !exists); err != nil {
return err
} else if skip {
return nil
}
// Create a fresh template
if !exists {
return store.CreateTemplate(ctx, pl.s, res)
}
// Update existing template
switch n.cfg.OnExisting {
case resource.Skip:
return nil
case resource.MergeLeft:
res = mergeTemplates(n.t, res)
case resource.MergeRight:
res = mergeTemplates(res, n.t)
}
err = store.UpdateTemplate(ctx, pl.s, res)
if err != nil {
return err
}
n.res.Res = res
return nil
}

View File

@ -0,0 +1,20 @@
package store
import (
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/system/types"
)
func newTemplate(t *types.Template) *template {
return &template{
t: t,
}
}
// MarshalEnvoy converts the template struct to a resource
func (u *template) MarshalEnvoy() ([]resource.Interface, error) {
return envoy.CollectNodes(
resource.NewTemplate(u.t),
)
}

View File

@ -52,7 +52,10 @@ func (y *decoder) Decode(ctx context.Context, r io.Reader, dctx *envoy.DecoderOp
return nil, err
}
return doc.Decode(ctx)
aaa, err := doc.Decode(ctx)
return aaa, err
}
// Checks validity of ref node and sets the value to given arg ptr

View File

@ -15,6 +15,7 @@ type (
compose *compose
roles roleSet
users userSet
templates templateSet
applications applicationSet
settings settingSet
rbac rbacRuleSet
@ -40,6 +41,9 @@ func (doc *Document) UnmarshalYAML(n *yaml.Node) (err error) {
case "users":
return v.Decode(&doc.users)
case "templates":
return v.Decode(&doc.templates)
case "applications":
return v.Decode(&doc.applications)
@ -85,6 +89,15 @@ func (doc *Document) MarshalYAML() (interface{}, error) {
}
}
if doc.templates != nil && len(doc.templates) > 0 {
doc.templates.ConfigureEncoder(doc.cfg)
dn, err = encodeResource(dn, "templates", doc.templates, doc.cfg.MappedOutput, "handle")
if err != nil {
return nil, err
}
}
if doc.applications != nil && len(doc.applications) > 0 {
doc.applications.ConfigureEncoder(doc.cfg)
@ -133,6 +146,9 @@ func (doc *Document) Decode(ctx context.Context) ([]resource.Interface, error) {
if doc.users != nil {
mm = append(mm, doc.users)
}
if doc.templates != nil {
mm = append(mm, doc.templates)
}
if doc.applications != nil {
mm = append(mm, doc.applications)
}
@ -236,6 +252,15 @@ func (doc *Document) AddUser(u *user) {
doc.users = append(doc.users, u)
}
// AddTemplate adds a new template to the document
func (doc *Document) AddTemplate(u *template) {
if doc.templates == nil {
doc.templates = make(templateSet, 0, 20)
}
doc.templates = append(doc.templates, u)
}
// AddApplication adds a new application to the document
func (doc *Document) AddApplication(a *application) {
if doc.applications == nil {

View File

@ -125,6 +125,8 @@ func (ye *yamlEncoder) Prepare(ctx context.Context, ee ...*envoy.ResourceState)
err = f(roleFromResource(res, ye.cfg), e)
case *resource.User:
err = f(userFromResource(res, ye.cfg), e)
case *resource.Template:
err = f(templateFromResource(res, ye.cfg), e)
case *resource.Application:
err = f(applicationFromResource(res, ye.cfg), e)
case *resource.Setting:

View File

@ -0,0 +1,25 @@
package yaml
import (
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
template struct {
res *types.Template
ts *resource.Timestamps
envoyConfig *resource.EnvoyConfig
encoderConfig *EncoderConfig
rbac rbacRuleSet
}
templateSet []*template
)
func (nn templateSet) ConfigureEncoder(cfg *EncoderConfig) {
for _, n := range nn {
n.encoderConfig = cfg
}
}

View File

@ -0,0 +1,85 @@
package yaml
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/pkg/envoy/util"
)
func templateFromResource(r *resource.Template, cfg *EncoderConfig) *template {
return &template{
res: r.Res,
encoderConfig: cfg,
}
}
// Prepare prepares the template to be encoded
//
// Any validation, additional constraining should be performed here.
func (u *template) Prepare(ctx context.Context, state *envoy.ResourceState) (err error) {
us, ok := state.Res.(*resource.Template)
if !ok {
return encoderErrInvalidResource(resource.TEMPLATE_RESOURCE_TYPE, state.Res.ResourceType())
}
u.res = us.Res
return nil
}
// Encode encodes the template to the document
//
// Encode is allowed to do some data manipulation, but no resource constraints
// should be changed.
func (u *template) Encode(ctx context.Context, doc *Document, state *envoy.ResourceState) (err error) {
if u.res.ID <= 0 {
u.res.ID = util.NextID()
}
// Encode timestamps
u.ts, err = resource.MakeCUDASTimestamps(&u.res.CreatedAt, u.res.UpdatedAt, u.res.DeletedAt, nil, nil).
Model(u.encoderConfig.TimeLayout, u.encoderConfig.Timezone)
if err != nil {
return err
}
// @todo implement resource skipping?
doc.AddTemplate(u)
return
}
func (c *template) MarshalYAML() (interface{}, error) {
var err error
meta, err := makeMap(
"short", c.res.Meta.Short,
"description", c.res.Meta.Description,
)
if err != nil {
return nil, err
}
nsn, err := makeMap(
"handle", c.res.Handle,
"language", c.res.Language,
"type", c.res.Type,
"partial", c.res.Partial,
"template", c.res.Template,
"meta", meta,
"Labels", c.res.Labels,
)
if err != nil {
return nil, err
}
nsn, err = mapTimestamps(nsn, c.ts)
if err != nil {
return nil, err
}
return nsn, nil
}

View File

@ -0,0 +1,95 @@
package yaml
import (
"github.com/cortezaproject/corteza-server/pkg/envoy"
"github.com/cortezaproject/corteza-server/pkg/envoy/resource"
"github.com/cortezaproject/corteza-server/pkg/y7s"
"github.com/cortezaproject/corteza-server/system/types"
"gopkg.in/yaml.v3"
)
func (wset *templateSet) UnmarshalYAML(n *yaml.Node) error {
return y7s.Each(n, func(k, v *yaml.Node) (err error) {
var (
wrap = &template{}
)
if v == nil {
return y7s.NodeErr(n, "malformed template definition")
}
wrap.res = &types.Template{}
switch v.Kind {
case yaml.MappingNode:
if err = v.Decode(&wrap); err != nil {
return
}
default:
return y7s.NodeErr(n, "expecting scalar or map with template definitions")
}
if err = decodeRef(k, "template handle", &wrap.res.Handle); err != nil {
return err
}
*wset = append(*wset, wrap)
return
})
}
func (wset templateSet) MarshalEnvoy() ([]resource.Interface, error) {
nn := make([]resource.Interface, 0, len(wset))
for _, res := range wset {
if tmp, err := res.MarshalEnvoy(); err != nil {
return nil, err
} else {
nn = append(nn, tmp...)
}
}
return nn, nil
}
func (wrap *template) UnmarshalYAML(n *yaml.Node) (err error) {
if !y7s.IsKind(n, yaml.MappingNode) {
return y7s.NodeErr(n, "template definition must be a map")
}
if wrap.res == nil {
wrap.res = &types.Template{}
}
if err = n.Decode(&wrap.res); err != nil {
return
}
if wrap.rbac, err = decodeRbac(n); err != nil {
return
}
if wrap.envoyConfig, err = decodeEnvoyConfig(n); err != nil {
return
}
if wrap.ts, err = decodeTimestamps(n); err != nil {
return
}
return nil
}
func (wrap template) MarshalEnvoy() ([]resource.Interface, error) {
rs := resource.NewTemplate(wrap.res)
rs.SetTimestamps(wrap.ts)
rs.SetConfig(wrap.envoyConfig)
return envoy.CollectNodes(
rs,
wrap.rbac.bindResource(rs),
)
}

View File

@ -1,18 +0,0 @@
settings:
auth.mail.email-confirmation.subject.en: Confirm your email address
auth.mail.email-confirmation.body.en: |-
{{.EmailHeaderEn}}
<h2 style="color: #568ba2;text-align: center;">Confirm your email address</h2>
<p>Hello,</p>
<p>Follow <a href="{{ .URL }}" style="color:#568ba2;">this link</a> to confirm your email address.</p>
<p>You will be logged-in after successful confirmation.</p>
{{.EmailFooterEn}}
auth.mail.password-reset.subject.en: Reset your password
auth.mail.password-reset.body.en: |-
{{.EmailHeaderEn}}
<h2 style="color: #568ba2;text-align: center;">Reset your password</h2>
<p>Hello,</p>
<p>Follow <a href="{{ .URL }}" style="color:#568ba2;">this link</a> and reset your password.</p>
<p>You will be logged-in after successful reset.</p>
{{.EmailFooterEn}}

View File

@ -0,0 +1,53 @@
templates:
auth_email_password_reset_subject:
type: text/plain
meta:
short: Password reset subject
template: Reset your password
auth_email_password_reset_content:
type: text/html
meta:
short: Password reset content
template: |-
{{template "email_general_header" .}}
<h2 style="color: #568ba2;text-align: center;">Reset your password</h2>
<p>Hello,</p>
<p>Follow <a href="{{ .URL }}" style="color:#568ba2;">this link</a> and reset your password.</p>
<p>You will be logged-in after successful reset.</p>
{{template "email_general_footer" .}}
auth_email_email_confirm_subject:
type: text/plain
meta:
short: Email confirmation subject
template: Reset your password
auth_email_email_confirm_content:
type: text/html
meta:
short: Email confirmation content
template: |-
{{template "email_general_header" .}}
<h2 style="color: #568ba2;text-align: center;">Confirm your email address</h2>
<p>Hello,</p>
<p>Follow <a href="{{ .URL }}" style="color:#568ba2;">this link</a> to confirm your email address.</p>
<p>You will be logged-in after successful confirmation.</p>
{{template "email_general_footer" .}}
auth_email_email_mfa_subject:
type: text/plain
meta:
short: MFA login code subject
template: Login code
auth_email_email_mfa_content:
type: text/html
meta:
short: MFA login code content
template: |-
{{template "email_general_header" .}}
<h2 style="color: #568ba2;text-align: center;">Reset your password</h2>
<p>Hello,</p>
<p>Enter this code into your login form: <code>{{.Code}}</code></p>
{{template "email_general_footer" .}}

View File

@ -0,0 +1,56 @@
templates:
email_general_header:
type: text/html
partial: true
meta:
short: General template header
description: General template header to use with system email notifications
template: |-
<div style="width:100%;min-height:100%;margin:0;padding:0;color:#3a393c;font-size:12px;line-height:18px;font-family:Verdana,Arial,sans-serif">
<table width="100%" align="center" style="width:100%;height:100%;border-collapse:collapse;border:0;padding:60px" border="0" cellspacing="0" cellpadding="0" summary="">
<tbody>
<tr>
<td valign="top" align="center" style="padding: 20px 0;">
<table width="800" cellspacing="0" cellpadding="0" border="0">
<tbody>
<tr>
<td width="800" bgcolor="#ffffff" style="color:#3a393c;font-size:14px;line-height:20px;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;text-align:left">
<table width="800" cellspacing="0" cellpadding="0" border="0">
<tbody>
<tr style="background-color:#ffffff;height:50px;">
<td style="border-bottom:2px solid #568ba2;">
<a href="{{ .BaseURL }}" style="text-decoration:none" target="_blank">
<img src="{{ .Logo }}" style="display: block;margin: 0 auto;padding: 10px;">
</a>
</td>
</tr>
<tr>
<td width="800" style="padding:40px 30px">
email_general_footer:
type: text/html
partial: true
meta:
short: General template footer
description: General template footer to use with system email notifications
template: |-
</td>
</tr>
<tr>
<td style="padding:30px;border-top: 1px solid #F3F3F5">
<p>If you have any questions, please contact <a href="mailto:{{ .SignatureEmail }}" style="color:#568ba2;">{{ .SignatureEmail }}</a>.</p>
<p>Kind regards, <br>
{{ .SignatureName }}</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -92,6 +92,7 @@ func truncateStore(ctx context.Context, s store.Storer, t *testing.T) {
s.TruncateRoles(ctx),
s.TruncateUsers(ctx),
s.TruncateTemplates(ctx),
s.TruncateApplications(ctx),
s.TruncateSettings(ctx),
s.TruncateRbacRules(ctx),

View File

@ -447,6 +447,32 @@ func TestStoreYaml_base(t *testing.T) {
},
},
{
name: "base templates",
pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) {
sTestTemplate(ctx, t, s, "base")
df := su.NewDecodeFilter().
Templates(&stypes.TemplateFilter{
Handle: "base_template",
})
return nil, df
},
check: func(ctx context.Context, s store.Storer, req *require.Assertions) {
tpl, err := store.LookupTemplateByHandle(ctx, s, "base_template")
req.NoError(err)
req.Equal("base_template", tpl.Handle)
req.Equal(stypes.DocumentTypeHTML, tpl.Type)
req.Equal(true, tpl.Partial)
req.Equal("base_short", tpl.Meta.Short)
req.Equal("base_description", tpl.Meta.Description)
req.Equal("base_template content", tpl.Template)
req.Equal(createdAt.Format(time.RFC3339), tpl.CreatedAt.Format(time.RFC3339))
req.Equal(updatedAt.Format(time.RFC3339), tpl.UpdatedAt.Format(time.RFC3339))
},
},
{
name: "base applications",
pre: func(ctx context.Context, s store.Storer) (error, *su.DecodeFilter) {

View File

@ -36,6 +36,29 @@ func sTestUser(ctx context.Context, t *testing.T, s store.Storer, pfx string) *t
return usr
}
func sTestTemplate(ctx context.Context, t *testing.T, s store.Storer, pfx string) *types.Template {
tpl := &types.Template{
ID: su.NextID(),
Handle: pfx + "_template",
Type: types.DocumentTypeHTML,
Partial: true,
Meta: types.TemplateMeta{
Short: pfx + "_short",
Description: pfx + "_description",
},
Template: pfx + "_template content",
CreatedAt: createdAt,
UpdatedAt: &updatedAt,
}
err := store.CreateTemplate(ctx, s, tpl)
if err != nil {
t.Fatal(err)
}
return tpl
}
func sTestRole(ctx context.Context, t *testing.T, s store.Storer, pfx string) *types.Role {
rl := &types.Role{
ID: su.NextID(),

View File

@ -0,0 +1,30 @@
templates:
email_general_header:
type: text/html
partial: true
meta:
short: General template header
description: General template header to use with system email notifications
template: <p>header</p>
email_general_footer:
type: text/html
partial: true
meta:
short: General template footer
description: General template footer to use with system email notifications
template: <p>footer</p>
email_general_content:
type: text/html
meta:
short: General template content
description: General template content to use with system email notifications
template: <p>content</p>
email_general_subject:
type: text/plain
meta:
short: General template subject
description: General template subject to use with system email notifications
template: content

View File

@ -11,6 +11,7 @@ import (
su "github.com/cortezaproject/corteza-server/pkg/envoy/store"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/cortezaproject/corteza-server/store"
stypes "github.com/cortezaproject/corteza-server/system/types"
"github.com/stretchr/testify/require"
)
@ -169,6 +170,12 @@ func TestYamlStore_provision(t *testing.T) {
req.Equal("1", sr.Values.Get("s2", 0).Value)
})
t.Run("templates", func(t *testing.T) {
tpls, _, err := store.SearchTemplates(ctx, s, stypes.TemplateFilter{})
req.NoError(err)
req.Len(tpls, 4)
})
truncateStore(ctx, s, t)
})