From b7e24f264bb0306721dcb9e6871d8de316dd33e8 Mon Sep 17 00:00:00 2001 From: Semyon Krestyaninov <krestyaninov@perx.ru> Date: Wed, 11 Jun 2025 13:23:57 +0300 Subject: [PATCH] wip --- template/builder.go | 49 +++++--- template/builder_test.go | 261 +++++++++++++++++++++++---------------- 2 files changed, 192 insertions(+), 118 deletions(-) diff --git a/template/builder.go b/template/builder.go index 64530be6..c6066226 100644 --- a/template/builder.go +++ b/template/builder.go @@ -3,7 +3,9 @@ package template import ( "bytes" "context" - "text/template" + html "html/template" + "io" + text "text/template" "git.perx.ru/perxis/perxis-go/pkg/collections" "git.perx.ru/perxis/perxis-go/pkg/content" @@ -11,32 +13,57 @@ import ( "git.perx.ru/perxis/perxis-go/pkg/spaces" ) +type Template interface { + Execute(w io.Writer, data any) error +} + type Builder struct { ctx context.Context cnt *content.Content SpaceID string EnvID string CollID string - data map[string]interface{} + data map[string]any // Для кеширования запросов space *spaces.Space environment *environments.Environment collection *collections.Collection + + // templateFunc парсит строку и возвращает шаблон для подстановки значений + templateFunc func(data string) (Template, error) } -func NewBuilder(cnt *content.Content, space, env, col string) *Builder { - return &Builder{ +func NewBuilder(cnt *content.Content, space, env, col string) Builder { + b := Builder{ ctx: context.Background(), cnt: cnt, SpaceID: space, EnvID: env, CollID: col, } + b.templateFunc = func(data string) (Template, error) { + return text.New("main_text").Funcs(b.getFuncs()).Parse(data) + } + return b } -func (b *Builder) getFuncs() template.FuncMap { - return template.FuncMap{ +func NewHTMLBuilder(cnt *content.Content, space, env, col string) Builder { + b := Builder{ + ctx: context.Background(), + cnt: cnt, + SpaceID: space, + EnvID: env, + CollID: col, + } + b.templateFunc = func(data string) (Template, error) { + return html.New("main_html").Funcs(b.getFuncs()).Parse(data) + } + return b +} + +func (b *Builder) getFuncs() map[string]any { + return map[string]any{ "lookup": getLookup(b), "system": getSystem(b), } @@ -84,14 +111,9 @@ func (b *Builder) Context() context.Context { return b.ctx } -func (b *Builder) Template() *template.Template { - return template.New("main").Funcs(b.getFuncs()) -} - func (b *Builder) Execute(str string, data ...any) (string, error) { - t := b.Template() buf := new(bytes.Buffer) - t, err := t.Parse(str) + t, err := b.templateFunc(str) if err != nil { return "", err } @@ -102,14 +124,13 @@ func (b *Builder) Execute(str string, data ...any) (string, error) { } func (b *Builder) ExecuteList(str []string, data ...any) ([]string, error) { - t := b.Template() result := make([]string, len(str)) buffer := new(bytes.Buffer) for i, tmpl := range str { if tmpl == "" { continue } - t, err := t.Parse(tmpl) + t, err := b.templateFunc(tmpl) if err != nil { return []string{}, err } diff --git a/template/builder_test.go b/template/builder_test.go index 128f7064..d6cb0d57 100644 --- a/template/builder_test.go +++ b/template/builder_test.go @@ -19,16 +19,17 @@ import ( func TestBuilder_Execute(t *testing.T) { tests := []struct { - name string - SpaceID string - EnvID string - CollID string - str string - data any - want any - wantErr bool + name string + SpaceID string + EnvID string + CollID string + str string + data any + want any + htmlBuilder bool + wantErr bool - getCnt func() (cnt *content.Content, assertExpectations func(t *testing.T)) + getCnt func(t *testing.T) *content.Content }{ {name: "error", str: "hello {{ .a }}", data: "world", want: "", wantErr: true}, {name: "empty", str: "", data: "", want: "", wantErr: false}, @@ -36,100 +37,164 @@ func TestBuilder_Execute(t *testing.T) { {name: "#2", str: "{{ . }}", data: "world", want: "world", wantErr: false}, {name: "#3 ", str: "", data: "world", want: "", wantErr: false}, {name: "#4 ", str: "hello", data: "world", want: "hello", wantErr: false}, - {name: "lookup", SpaceID: "space", EnvID: "env", str: "hello, {{ lookup \"secrets.dev.key\" }}", data: "", want: "hello, Luk", wantErr: false, getCnt: func() (*content.Content, func(t *testing.T)) { - itemsSvc := &mocksitems.Items{} - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{ - "id": "dev", - "key": "Luk", - }, - }, nil).Once() - return &content.Content{Items: itemsSvc}, func(t *testing.T) { itemsSvc.AssertExpectations(t) } - }}, - {name: "lookup with slice", SpaceID: "space", EnvID: "env", str: "numbers {{ lookup \"secrets.dev.slice\" }}", data: "", want: "numbers [1 2 3]", wantErr: false, getCnt: func() (*content.Content, func(t *testing.T)) { - itemsSvc := &mocksitems.Items{} - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{ - "id": "dev", - "slice": []int{1, 2, 3}, - }, - }, nil).Once() - return &content.Content{Items: itemsSvc}, func(t *testing.T) { itemsSvc.AssertExpectations(t) } - }}, - {name: "lookup with empty Data", SpaceID: "space", EnvID: "env", str: "numbers {{ lookup \"secrets.dev.slice\" }}", data: "", want: "numbers <no value>", wantErr: false, getCnt: func() (*content.Content, func(t *testing.T)) { - itemsSvc := &mocksitems.Items{} - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{}, - }, nil).Once() - return &content.Content{Items: itemsSvc}, func(t *testing.T) { itemsSvc.AssertExpectations(t) } - }}, - {name: "lookup with incorrect field", SpaceID: "space", EnvID: "env", str: "hello {{ lookup \"secrets.dev.incorrect\" }}", data: "", want: "hello <no value>", wantErr: false, getCnt: func() (*content.Content, func(t *testing.T)) { - itemsSvc := &mocksitems.Items{} - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{ - ID: "dev", - SpaceID: "space", - EnvID: "env", - CollectionID: "secrets", - Data: map[string]interface{}{ - "id": "dev", - "key": "1234", - }, - }, nil).Once() - return &content.Content{Items: itemsSvc}, func(t *testing.T) { itemsSvc.AssertExpectations(t) } - }}, - {name: "lookup not found", SpaceID: "space", EnvID: "env", str: "hello {{ lookup \"secrets.prod.pass\" }}", data: "", want: "", wantErr: true, getCnt: func() (*content.Content, func(t *testing.T)) { - itemsSvc := &mocksitems.Items{} - itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "prod").Return(nil, errors.New("not found")).Once() - return &content.Content{Items: itemsSvc}, func(t *testing.T) { itemsSvc.AssertExpectations(t) } - }}, + { + name: "lookup", + SpaceID: "space", + EnvID: "env", + str: "hello, {{ lookup \"secrets.dev.key\" }}", + data: "", + want: "hello, Luk", + wantErr: false, + getCnt: func(t *testing.T) *content.Content { + itemsSvc := mocksitems.NewItems(t) + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev"). + Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "key": "Luk", + }, + }, nil).Once() + return &content.Content{Items: itemsSvc} + }, + }, + { + name: "lookup with slice", + SpaceID: "space", + EnvID: "env", + str: "numbers {{ lookup \"secrets.dev.slice\" }}", + data: "", + want: "numbers [1 2 3]", + wantErr: false, + getCnt: func(t *testing.T) *content.Content { + itemsSvc := mocksitems.NewItems(t) + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev"). + Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "slice": []int{1, 2, 3}, + }, + }, nil).Once() + return &content.Content{Items: itemsSvc} + }, + }, + { + name: "lookup with empty Data", + SpaceID: "space", + EnvID: "env", + str: "numbers {{ lookup \"secrets.dev.slice\" }}", + data: "", + want: "numbers <no value>", + wantErr: false, + getCnt: func(t *testing.T) *content.Content { + itemsSvc := mocksitems.NewItems(t) + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev"). + Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{}, + }, nil).Once() + return &content.Content{Items: itemsSvc} + }, + }, + { + name: "lookup with incorrect field", + SpaceID: "space", + EnvID: "env", + str: "hello {{ lookup \"secrets.dev.incorrect\" }}", + data: "", + want: "hello <no value>", + wantErr: false, + getCnt: func(t *testing.T) *content.Content { + itemsSvc := mocksitems.NewItems(t) + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev"). + Return(&items.Item{ + ID: "dev", + SpaceID: "space", + EnvID: "env", + CollectionID: "secrets", + Data: map[string]interface{}{ + "id": "dev", + "key": "1234", + }, + }, nil).Once() + return &content.Content{Items: itemsSvc} + }, + }, + { + name: "lookup not found", + SpaceID: "space", + EnvID: "env", + str: "hello {{ lookup \"secrets.prod.pass\" }}", + data: "", + want: "", + wantErr: true, + getCnt: func(t *testing.T) *content.Content { + itemsSvc := mocksitems.NewItems(t) + itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "prod"). + Return(nil, errors.New("not found")).Once() + return &content.Content{Items: itemsSvc} + }, + }, {name: "lookup without itemID", SpaceID: "space", EnvID: "env", str: "hello {{ lookup \"secrets.pass\" }}", data: "", want: "", wantErr: true}, {name: "system#1", SpaceID: "space", str: "hello {{ system.SpaceID }}", data: "", want: "hello space", wantErr: false}, - {name: "system space", SpaceID: "space", getCnt: func() (cnt *content.Content, assertExpectations func(t *testing.T)) { - spsSvc := &spsmocks.Spaces{} + {name: "system space", SpaceID: "space", getCnt: func(t *testing.T) *content.Content { + spsSvc := spsmocks.NewSpaces(t) spsSvc.On("Get", context.Background(), "space").Return(&spaces.Space{Description: "description"}, nil).Once() - return &content.Content{Spaces: spsSvc}, func(t *testing.T) { spsSvc.AssertExpectations(t) } + return &content.Content{Spaces: spsSvc} }, str: "{{ system.Space.Description }}", want: "description", wantErr: false}, - {name: "system environment", SpaceID: "space", EnvID: "env", getCnt: func() (cnt *content.Content, assertExpectations func(t *testing.T)) { - envsSvc := &envsmocks.Environments{} + {name: "system environment", SpaceID: "space", EnvID: "env", getCnt: func(t *testing.T) *content.Content { + envsSvc := envsmocks.NewEnvironments(t) envsSvc.On("Get", context.Background(), "space", "env").Return(&environments.Environment{Aliases: []string{"master"}}, nil).Once() - return &content.Content{Environments: envsSvc}, func(t *testing.T) { envsSvc.AssertExpectations(t) } + return &content.Content{Environments: envsSvc} }, str: "{{ system.Environment.Aliases }}", want: "[master]", wantErr: false}, - {name: "system collection", SpaceID: "space", EnvID: "env", CollID: "col", getCnt: func() (cnt *content.Content, assertExpectations func(t *testing.T)) { - collsSvc := &colsmocks.Collections{} - collsSvc.On("Get", context.Background(), "space", "env", "col").Return(&collections.Collection{Name: "cars"}, nil).Once() - return &content.Content{Collections: collsSvc}, func(t *testing.T) { collsSvc.AssertExpectations(t) } - }, str: "{{ system.Collection.Name }}", want: "cars", wantErr: false}, + { + name: "system collection", + SpaceID: "space", + EnvID: "env", + CollID: "col", + getCnt: func(t *testing.T) *content.Content { + collsSvc := colsmocks.NewCollections(t) + collsSvc.On("Get", context.Background(), "space", "env", "col"). + Return(&collections.Collection{Name: "cars"}, nil).Once() + return &content.Content{Collections: collsSvc} + }, + str: "{{ system.Collection.Name }}", + want: "cars", + wantErr: false, + }, {name: "system without account", SpaceID: "space", str: "hello {{ system.Organization.Name }}", want: "", wantErr: true}, + { + name: "with html builder", + str: "{{ . }}", + data: "<script>alert(localStorage.secret)</script>", + want: "<script>alert(localStorage.secret)</script>", + htmlBuilder: true, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var cnt *content.Content if tt.getCnt != nil { - var checkFn func(*testing.T) - cnt, checkFn = tt.getCnt() - defer checkFn(t) + cnt = tt.getCnt(t) } - b := &Builder{ - ctx: context.Background(), - cnt: cnt, - SpaceID: tt.SpaceID, - EnvID: tt.EnvID, - CollID: tt.CollID, + var b Builder + if tt.htmlBuilder { + b = NewHTMLBuilder(cnt, tt.SpaceID, tt.EnvID, tt.CollID) + } else { + b = NewBuilder(cnt, tt.SpaceID, tt.EnvID, tt.CollID) } - got, err := b.Execute(tt.str, tt.data) if tt.wantErr == true { assert.Error(t, err) @@ -187,16 +252,11 @@ func TestBuilder_ExecuteList(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - itemsSvc := &mocksitems.Items{} + itemsSvc := mocksitems.NewItems(t) if tt.itemsCall != nil { tt.itemsCall(itemsSvc) } - b := &Builder{ - ctx: context.Background(), - cnt: &content.Content{Items: itemsSvc}, - SpaceID: tt.SpaceID, - EnvID: tt.EnvID, - } + b := NewBuilder(&content.Content{Items: itemsSvc}, tt.SpaceID, tt.EnvID, "") got, err := b.ExecuteList(tt.str, tt.data) if tt.wantErr == true { @@ -205,7 +265,6 @@ func TestBuilder_ExecuteList(t *testing.T) { assert.NoError(t, err) } assert.Equal(t, tt.want, got) - itemsSvc.AssertExpectations(t) }) } } @@ -256,16 +315,11 @@ func TestBuilder_ExecuteMap(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - itemsSvc := &mocksitems.Items{} + itemsSvc := mocksitems.NewItems(t) if tt.itemsCall != nil { tt.itemsCall(itemsSvc) } - b := &Builder{ - ctx: context.Background(), - cnt: &content.Content{Items: itemsSvc}, - SpaceID: tt.SpaceID, - EnvID: tt.EnvID, - } + b := NewBuilder(&content.Content{Items: itemsSvc}, tt.SpaceID, tt.EnvID, "") got, err := b.ExecuteMap(tt.str, tt.data) if tt.wantErr == true { @@ -274,7 +328,6 @@ func TestBuilder_ExecuteMap(t *testing.T) { assert.NoError(t, err) } assert.Equal(t, tt.want, got) - itemsSvc.AssertExpectations(t) }) } } -- GitLab