Skip to content

Commit a163c53

Browse files
authored
Refactor template & test related code (#32938)
Move some legacy code from "base" package to proper packages.
1 parent afe314f commit a163c53

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+479
-476
lines changed

models/migrations/base/tests.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import (
1313
"testing"
1414

1515
"code.gitea.io/gitea/models/unittest"
16-
"code.gitea.io/gitea/modules/base"
1716
"code.gitea.io/gitea/modules/git"
1817
"code.gitea.io/gitea/modules/setting"
18+
"code.gitea.io/gitea/modules/test"
1919
"code.gitea.io/gitea/modules/testlogger"
2020

2121
"github.com/stretchr/testify/require"
@@ -92,10 +92,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
9292
func MainTest(m *testing.M) {
9393
testlogger.Init()
9494

95-
giteaRoot := base.SetupGiteaRoot()
96-
if giteaRoot == "" {
97-
testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n")
98-
}
95+
giteaRoot := test.SetupGiteaRoot()
9996
giteaBinary := "gitea"
10097
if runtime.GOOS == "windows" {
10198
giteaBinary += ".exe"

models/unittest/testdb.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import (
1414
"code.gitea.io/gitea/models/db"
1515
"code.gitea.io/gitea/models/system"
1616
"code.gitea.io/gitea/modules/auth/password/hash"
17-
"code.gitea.io/gitea/modules/base"
1817
"code.gitea.io/gitea/modules/cache"
1918
"code.gitea.io/gitea/modules/git"
2019
"code.gitea.io/gitea/modules/log"
2120
"code.gitea.io/gitea/modules/setting"
2221
"code.gitea.io/gitea/modules/setting/config"
2322
"code.gitea.io/gitea/modules/storage"
23+
"code.gitea.io/gitea/modules/test"
2424
"code.gitea.io/gitea/modules/util"
2525

2626
"github.com/stretchr/testify/assert"
@@ -235,5 +235,5 @@ func PrepareTestEnv(t testing.TB) {
235235
assert.NoError(t, PrepareTestDatabase())
236236
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
237237
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
238-
base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
238+
test.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
239239
}

modules/base/base.go

-9
This file was deleted.

modules/base/tool.go

-49
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ import (
1313
"errors"
1414
"fmt"
1515
"hash"
16-
"os"
17-
"path/filepath"
18-
"runtime"
1916
"strconv"
2017
"strings"
2118
"time"
@@ -189,49 +186,3 @@ func EntryIcon(entry *git.TreeEntry) string {
189186

190187
return "file"
191188
}
192-
193-
// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
194-
func SetupGiteaRoot() string {
195-
giteaRoot := os.Getenv("GITEA_ROOT")
196-
if giteaRoot == "" {
197-
_, filename, _, _ := runtime.Caller(0)
198-
giteaRoot = strings.TrimSuffix(filename, "modules/base/tool.go")
199-
wd, err := os.Getwd()
200-
if err != nil {
201-
rel, err := filepath.Rel(giteaRoot, wd)
202-
if err != nil && strings.HasPrefix(filepath.ToSlash(rel), "../") {
203-
giteaRoot = wd
204-
}
205-
}
206-
if _, err := os.Stat(filepath.Join(giteaRoot, "gitea")); os.IsNotExist(err) {
207-
giteaRoot = ""
208-
} else if err := os.Setenv("GITEA_ROOT", giteaRoot); err != nil {
209-
giteaRoot = ""
210-
}
211-
}
212-
return giteaRoot
213-
}
214-
215-
// FormatNumberSI format a number
216-
func FormatNumberSI(data any) string {
217-
var num int64
218-
if num1, ok := data.(int64); ok {
219-
num = num1
220-
} else if num1, ok := data.(int); ok {
221-
num = int64(num1)
222-
} else {
223-
return ""
224-
}
225-
226-
if num < 1000 {
227-
return fmt.Sprintf("%d", num)
228-
} else if num < 1000000 {
229-
num2 := float32(num) / float32(1000.0)
230-
return fmt.Sprintf("%.1fk", num2)
231-
} else if num < 1000000000 {
232-
num2 := float32(num) / float32(1000000.0)
233-
return fmt.Sprintf("%.1fM", num2)
234-
}
235-
num2 := float32(num) / float32(1000000000.0)
236-
return fmt.Sprintf("%.1fG", num2)
237-
}

modules/base/tool_test.go

-15
Original file line numberDiff line numberDiff line change
@@ -169,18 +169,3 @@ func TestInt64sToStrings(t *testing.T) {
169169
}
170170

171171
// TODO: Test EntryIcon
172-
173-
func TestSetupGiteaRoot(t *testing.T) {
174-
t.Setenv("GITEA_ROOT", "test")
175-
assert.Equal(t, "test", SetupGiteaRoot())
176-
t.Setenv("GITEA_ROOT", "")
177-
assert.NotEqual(t, "test", SetupGiteaRoot())
178-
}
179-
180-
func TestFormatNumberSI(t *testing.T) {
181-
assert.Equal(t, "125", FormatNumberSI(int(125)))
182-
assert.Equal(t, "1.3k", FormatNumberSI(int64(1317)))
183-
assert.Equal(t, "21.3M", FormatNumberSI(21317675))
184-
assert.Equal(t, "45.7G", FormatNumberSI(45721317675))
185-
assert.Equal(t, "", FormatNumberSI("test"))
186-
}

modules/templates/helper.go

+3-33
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"html"
1010
"html/template"
1111
"net/url"
12-
"reflect"
1312
"strings"
1413
"time"
1514

@@ -69,7 +68,7 @@ func NewFuncMap() template.FuncMap {
6968
// -----------------------------------------------------------------
7069
// time / number / format
7170
"FileSize": base.FileSize,
72-
"CountFmt": base.FormatNumberSI,
71+
"CountFmt": countFmt,
7372
"Sec2Time": util.SecToTime,
7473

7574
"TimeEstimateString": timeEstimateString,
@@ -239,29 +238,8 @@ func iif(condition any, vals ...any) any {
239238
}
240239

241240
func isTemplateTruthy(v any) bool {
242-
if v == nil {
243-
return false
244-
}
245-
246-
rv := reflect.ValueOf(v)
247-
switch rv.Kind() {
248-
case reflect.Bool:
249-
return rv.Bool()
250-
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
251-
return rv.Int() != 0
252-
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
253-
return rv.Uint() != 0
254-
case reflect.Float32, reflect.Float64:
255-
return rv.Float() != 0
256-
case reflect.Complex64, reflect.Complex128:
257-
return rv.Complex() != 0
258-
case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
259-
return rv.Len() > 0
260-
case reflect.Struct:
261-
return true
262-
default:
263-
return !rv.IsNil()
264-
}
241+
truth, _ := template.IsTrue(v)
242+
return truth
265243
}
266244

267245
// evalTokens evaluates the expression by tokens and returns the result, see the comment of eval.Expr for details.
@@ -286,14 +264,6 @@ func userThemeName(user *user_model.User) string {
286264
return setting.UI.DefaultTheme
287265
}
288266

289-
func timeEstimateString(timeSec any) string {
290-
v, _ := util.ToInt64(timeSec)
291-
if v == 0 {
292-
return ""
293-
}
294-
return util.TimeEstimateString(v)
295-
}
296-
297267
// QueryBuild builds a query string from a list of key-value pairs.
298268
// It omits the nil and empty strings, but it doesn't omit other zero values,
299269
// because the zero value of number types may have a meaning.

modules/templates/helper_test.go

+37-21
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"testing"
1010

11+
"code.gitea.io/gitea/modules/htmlutil"
1112
"code.gitea.io/gitea/modules/util"
1213

1314
"github.com/stretchr/testify/assert"
@@ -65,31 +66,12 @@ func TestSanitizeHTML(t *testing.T) {
6566
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
6667
}
6768

68-
func TestTemplateTruthy(t *testing.T) {
69+
func TestTemplateIif(t *testing.T) {
6970
tmpl := template.New("test")
7071
tmpl.Funcs(template.FuncMap{"Iif": iif})
7172
template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`))
7273

73-
cases := []any{
74-
nil, false, true, "", "string", 0, 1,
75-
byte(0), byte(1), int64(0), int64(1), float64(0), float64(1),
76-
complex(0, 0), complex(1, 0),
77-
(chan int)(nil), make(chan int),
78-
(func())(nil), func() {},
79-
util.ToPointer(0), util.ToPointer(util.ToPointer(0)),
80-
util.ToPointer(1), util.ToPointer(util.ToPointer(1)),
81-
[0]int{},
82-
[1]int{0},
83-
[]int(nil),
84-
[]int{},
85-
[]int{0},
86-
map[any]any(nil),
87-
map[any]any{},
88-
map[any]any{"k": "v"},
89-
(*struct{})(nil),
90-
struct{}{},
91-
util.ToPointer(struct{}{}),
92-
}
74+
cases := []any{nil, false, true, "", "string", 0, 1}
9375
w := &strings.Builder{}
9476
truthyCount := 0
9577
for i, v := range cases {
@@ -102,3 +84,37 @@ func TestTemplateTruthy(t *testing.T) {
10284
}
10385
assert.True(t, truthyCount != 0 && truthyCount != len(cases))
10486
}
87+
88+
func TestTemplateEscape(t *testing.T) {
89+
execTmpl := func(code string) string {
90+
tmpl := template.New("test")
91+
tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlutil.HTMLFormat})
92+
template.Must(tmpl.Parse(code))
93+
w := &strings.Builder{}
94+
assert.NoError(t, tmpl.Execute(w, nil))
95+
return w.String()
96+
}
97+
98+
t.Run("Golang URL Escape", func(t *testing.T) {
99+
// Golang template considers "href", "*src*", "*uri*", "*url*" (and more) ... attributes as contentTypeURL and does auto-escaping
100+
actual := execTmpl(`<a href="?a={{"%"}}"></a>`)
101+
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
102+
actual = execTmpl(`<a data-xxx-url="?a={{"%"}}"></a>`)
103+
assert.Equal(t, `<a data-xxx-url="?a=%25"></a>`, actual)
104+
})
105+
t.Run("Golang URL No-escape", func(t *testing.T) {
106+
// non-URL content isn't auto-escaped
107+
actual := execTmpl(`<a data-link="?a={{"%"}}"></a>`)
108+
assert.Equal(t, `<a data-link="?a=%"></a>`, actual)
109+
})
110+
t.Run("QueryBuild", func(t *testing.T) {
111+
actual := execTmpl(`<a href="{{QueryBuild "?" "a" "%"}}"></a>`)
112+
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
113+
actual = execTmpl(`<a href="?{{QueryBuild "a" "%"}}"></a>`)
114+
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
115+
})
116+
t.Run("HTMLFormat", func(t *testing.T) {
117+
actual := execTmpl("{{HTMLFormat `<a k=\"%s\">%s</a>` `\"` `<>`}}")
118+
assert.Equal(t, `<a k="&#34;">&lt;&gt;</a>`, actual)
119+
})
120+
}

modules/templates/htmlrenderer.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import (
2929

3030
type TemplateExecutor scopedtmpl.TemplateExecutor
3131

32+
type TplName string
33+
3234
type HTMLRender struct {
3335
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
3436
}
@@ -40,7 +42,8 @@ var (
4042

4143
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
4244

43-
func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive
45+
func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive
46+
name := string(tplName)
4447
if respWriter, ok := w.(http.ResponseWriter); ok {
4548
if respWriter.Header().Get("Content-Type") == "" {
4649
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")

modules/templates/util_format.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package templates
5+
6+
import (
7+
"fmt"
8+
9+
"code.gitea.io/gitea/modules/util"
10+
)
11+
12+
func timeEstimateString(timeSec any) string {
13+
v, _ := util.ToInt64(timeSec)
14+
if v == 0 {
15+
return ""
16+
}
17+
return util.TimeEstimateString(v)
18+
}
19+
20+
func countFmt(data any) string {
21+
// legacy code, not ideal, still used in some places
22+
num, err := util.ToInt64(data)
23+
if err != nil {
24+
return ""
25+
}
26+
if num < 1000 {
27+
return fmt.Sprintf("%d", num)
28+
} else if num < 1_000_000 {
29+
num2 := float32(num) / 1000.0
30+
return fmt.Sprintf("%.1fk", num2)
31+
} else if num < 1_000_000_000 {
32+
num2 := float32(num) / 1_000_000.0
33+
return fmt.Sprintf("%.1fM", num2)
34+
}
35+
num2 := float32(num) / 1_000_000_000.0
36+
return fmt.Sprintf("%.1fG", num2)
37+
}

modules/templates/util_format_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package templates
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestCountFmt(t *testing.T) {
13+
assert.Equal(t, "125", countFmt(125))
14+
assert.Equal(t, "1.3k", countFmt(int64(1317)))
15+
assert.Equal(t, "21.3M", countFmt(21317675))
16+
assert.Equal(t, "45.7G", countFmt(45721317675))
17+
assert.Equal(t, "", countFmt("test"))
18+
}

modules/test/utils.go

+21
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
package test
55

66
import (
7+
"fmt"
78
"net/http"
89
"net/http/httptest"
10+
"os"
11+
"path/filepath"
12+
"runtime"
913
"strings"
1014

1115
"code.gitea.io/gitea/modules/json"
16+
"code.gitea.io/gitea/modules/util"
1217
)
1318

1419
// RedirectURL returns the redirect URL of a http response.
@@ -41,3 +46,19 @@ func MockVariableValue[T any](p *T, v ...T) (reset func()) {
4146
}
4247
return func() { *p = old }
4348
}
49+
50+
// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
51+
func SetupGiteaRoot() string {
52+
giteaRoot := os.Getenv("GITEA_ROOT")
53+
if giteaRoot != "" {
54+
return giteaRoot
55+
}
56+
_, filename, _, _ := runtime.Caller(0)
57+
giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename)))
58+
fixturesDir := filepath.Join(giteaRoot, "models", "fixtures")
59+
if exist, _ := util.IsDir(fixturesDir); !exist {
60+
panic(fmt.Sprintf("fixtures directory not found: %s", fixturesDir))
61+
}
62+
_ = os.Setenv("GITEA_ROOT", giteaRoot)
63+
return giteaRoot
64+
}

0 commit comments

Comments
 (0)