Skip to content

Commit

Permalink
Refactor template & test related code (#32938)
Browse files Browse the repository at this point in the history
Move some legacy code from "base" package to proper packages.
  • Loading branch information
wxiaoguang authored Dec 22, 2024
1 parent afe314f commit a163c53
Show file tree
Hide file tree
Showing 109 changed files with 479 additions and 476 deletions.
7 changes: 2 additions & 5 deletions models/migrations/base/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
"testing"

"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/testlogger"

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

giteaRoot := base.SetupGiteaRoot()
if giteaRoot == "" {
testlogger.Fatalf("Environment variable $GITEA_ROOT not set\n")
}
giteaRoot := test.SetupGiteaRoot()
giteaBinary := "gitea"
if runtime.GOOS == "windows" {
giteaBinary += ".exe"
Expand Down
4 changes: 2 additions & 2 deletions models/unittest/testdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -235,5 +235,5 @@ func PrepareTestEnv(t testing.TB) {
assert.NoError(t, PrepareTestDatabase())
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
test.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
}
9 changes: 0 additions & 9 deletions modules/base/base.go

This file was deleted.

49 changes: 0 additions & 49 deletions modules/base/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import (
"errors"
"fmt"
"hash"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -189,49 +186,3 @@ func EntryIcon(entry *git.TreeEntry) string {

return "file"
}

// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
func SetupGiteaRoot() string {
giteaRoot := os.Getenv("GITEA_ROOT")
if giteaRoot == "" {
_, filename, _, _ := runtime.Caller(0)
giteaRoot = strings.TrimSuffix(filename, "modules/base/tool.go")
wd, err := os.Getwd()
if err != nil {
rel, err := filepath.Rel(giteaRoot, wd)
if err != nil && strings.HasPrefix(filepath.ToSlash(rel), "../") {
giteaRoot = wd
}
}
if _, err := os.Stat(filepath.Join(giteaRoot, "gitea")); os.IsNotExist(err) {
giteaRoot = ""
} else if err := os.Setenv("GITEA_ROOT", giteaRoot); err != nil {
giteaRoot = ""
}
}
return giteaRoot
}

// FormatNumberSI format a number
func FormatNumberSI(data any) string {
var num int64
if num1, ok := data.(int64); ok {
num = num1
} else if num1, ok := data.(int); ok {
num = int64(num1)
} else {
return ""
}

if num < 1000 {
return fmt.Sprintf("%d", num)
} else if num < 1000000 {
num2 := float32(num) / float32(1000.0)
return fmt.Sprintf("%.1fk", num2)
} else if num < 1000000000 {
num2 := float32(num) / float32(1000000.0)
return fmt.Sprintf("%.1fM", num2)
}
num2 := float32(num) / float32(1000000000.0)
return fmt.Sprintf("%.1fG", num2)
}
15 changes: 0 additions & 15 deletions modules/base/tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,3 @@ func TestInt64sToStrings(t *testing.T) {
}

// TODO: Test EntryIcon

func TestSetupGiteaRoot(t *testing.T) {
t.Setenv("GITEA_ROOT", "test")
assert.Equal(t, "test", SetupGiteaRoot())
t.Setenv("GITEA_ROOT", "")
assert.NotEqual(t, "test", SetupGiteaRoot())
}

func TestFormatNumberSI(t *testing.T) {
assert.Equal(t, "125", FormatNumberSI(int(125)))
assert.Equal(t, "1.3k", FormatNumberSI(int64(1317)))
assert.Equal(t, "21.3M", FormatNumberSI(21317675))
assert.Equal(t, "45.7G", FormatNumberSI(45721317675))
assert.Equal(t, "", FormatNumberSI("test"))
}
36 changes: 3 additions & 33 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"html"
"html/template"
"net/url"
"reflect"
"strings"
"time"

Expand Down Expand Up @@ -69,7 +68,7 @@ func NewFuncMap() template.FuncMap {
// -----------------------------------------------------------------
// time / number / format
"FileSize": base.FileSize,
"CountFmt": base.FormatNumberSI,
"CountFmt": countFmt,
"Sec2Time": util.SecToTime,

"TimeEstimateString": timeEstimateString,
Expand Down Expand Up @@ -239,29 +238,8 @@ func iif(condition any, vals ...any) any {
}

func isTemplateTruthy(v any) bool {
if v == nil {
return false
}

rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Bool:
return rv.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() != 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return rv.Uint() != 0
case reflect.Float32, reflect.Float64:
return rv.Float() != 0
case reflect.Complex64, reflect.Complex128:
return rv.Complex() != 0
case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
return rv.Len() > 0
case reflect.Struct:
return true
default:
return !rv.IsNil()
}
truth, _ := template.IsTrue(v)
return truth
}

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

func timeEstimateString(timeSec any) string {
v, _ := util.ToInt64(timeSec)
if v == 0 {
return ""
}
return util.TimeEstimateString(v)
}

// QueryBuild builds a query string from a list of key-value pairs.
// It omits the nil and empty strings, but it doesn't omit other zero values,
// because the zero value of number types may have a meaning.
Expand Down
58 changes: 37 additions & 21 deletions modules/templates/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/util"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -65,31 +66,12 @@ func TestSanitizeHTML(t *testing.T) {
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>`))
}

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

cases := []any{
nil, false, true, "", "string", 0, 1,
byte(0), byte(1), int64(0), int64(1), float64(0), float64(1),
complex(0, 0), complex(1, 0),
(chan int)(nil), make(chan int),
(func())(nil), func() {},
util.ToPointer(0), util.ToPointer(util.ToPointer(0)),
util.ToPointer(1), util.ToPointer(util.ToPointer(1)),
[0]int{},
[1]int{0},
[]int(nil),
[]int{},
[]int{0},
map[any]any(nil),
map[any]any{},
map[any]any{"k": "v"},
(*struct{})(nil),
struct{}{},
util.ToPointer(struct{}{}),
}
cases := []any{nil, false, true, "", "string", 0, 1}
w := &strings.Builder{}
truthyCount := 0
for i, v := range cases {
Expand All @@ -102,3 +84,37 @@ func TestTemplateTruthy(t *testing.T) {
}
assert.True(t, truthyCount != 0 && truthyCount != len(cases))
}

func TestTemplateEscape(t *testing.T) {
execTmpl := func(code string) string {
tmpl := template.New("test")
tmpl.Funcs(template.FuncMap{"QueryBuild": QueryBuild, "HTMLFormat": htmlutil.HTMLFormat})
template.Must(tmpl.Parse(code))
w := &strings.Builder{}
assert.NoError(t, tmpl.Execute(w, nil))
return w.String()
}

t.Run("Golang URL Escape", func(t *testing.T) {
// Golang template considers "href", "*src*", "*uri*", "*url*" (and more) ... attributes as contentTypeURL and does auto-escaping
actual := execTmpl(`<a href="?a={{"%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
actual = execTmpl(`<a data-xxx-url="?a={{"%"}}"></a>`)
assert.Equal(t, `<a data-xxx-url="?a=%25"></a>`, actual)
})
t.Run("Golang URL No-escape", func(t *testing.T) {
// non-URL content isn't auto-escaped
actual := execTmpl(`<a data-link="?a={{"%"}}"></a>`)
assert.Equal(t, `<a data-link="?a=%"></a>`, actual)
})
t.Run("QueryBuild", func(t *testing.T) {
actual := execTmpl(`<a href="{{QueryBuild "?" "a" "%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
actual = execTmpl(`<a href="?{{QueryBuild "a" "%"}}"></a>`)
assert.Equal(t, `<a href="?a=%25"></a>`, actual)
})
t.Run("HTMLFormat", func(t *testing.T) {
actual := execTmpl("{{HTMLFormat `<a k=\"%s\">%s</a>` `\"` `<>`}}")
assert.Equal(t, `<a k="&#34;">&lt;&gt;</a>`, actual)
})
}
5 changes: 4 additions & 1 deletion modules/templates/htmlrenderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (

type TemplateExecutor scopedtmpl.TemplateExecutor

type TplName string

type HTMLRender struct {
templates atomic.Pointer[scopedtmpl.ScopedTemplate]
}
Expand All @@ -40,7 +42,8 @@ var (

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

func (h *HTMLRender) HTML(w io.Writer, status int, name string, data any, ctx context.Context) error { //nolint:revive
func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive
name := string(tplName)
if respWriter, ok := w.(http.ResponseWriter); ok {
if respWriter.Header().Get("Content-Type") == "" {
respWriter.Header().Set("Content-Type", "text/html; charset=utf-8")
Expand Down
37 changes: 37 additions & 0 deletions modules/templates/util_format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package templates

import (
"fmt"

"code.gitea.io/gitea/modules/util"
)

func timeEstimateString(timeSec any) string {
v, _ := util.ToInt64(timeSec)
if v == 0 {
return ""
}
return util.TimeEstimateString(v)
}

func countFmt(data any) string {
// legacy code, not ideal, still used in some places
num, err := util.ToInt64(data)
if err != nil {
return ""
}
if num < 1000 {
return fmt.Sprintf("%d", num)
} else if num < 1_000_000 {
num2 := float32(num) / 1000.0
return fmt.Sprintf("%.1fk", num2)
} else if num < 1_000_000_000 {
num2 := float32(num) / 1_000_000.0
return fmt.Sprintf("%.1fM", num2)
}
num2 := float32(num) / 1_000_000_000.0
return fmt.Sprintf("%.1fG", num2)
}
18 changes: 18 additions & 0 deletions modules/templates/util_format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package templates

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestCountFmt(t *testing.T) {
assert.Equal(t, "125", countFmt(125))
assert.Equal(t, "1.3k", countFmt(int64(1317)))
assert.Equal(t, "21.3M", countFmt(21317675))
assert.Equal(t, "45.7G", countFmt(45721317675))
assert.Equal(t, "", countFmt("test"))
}
21 changes: 21 additions & 0 deletions modules/test/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
package test

import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
)

// RedirectURL returns the redirect URL of a http response.
Expand Down Expand Up @@ -41,3 +46,19 @@ func MockVariableValue[T any](p *T, v ...T) (reset func()) {
}
return func() { *p = old }
}

// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
func SetupGiteaRoot() string {
giteaRoot := os.Getenv("GITEA_ROOT")
if giteaRoot != "" {
return giteaRoot
}
_, filename, _, _ := runtime.Caller(0)
giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename)))
fixturesDir := filepath.Join(giteaRoot, "models", "fixtures")
if exist, _ := util.IsDir(fixturesDir); !exist {
panic(fmt.Sprintf("fixtures directory not found: %s", fixturesDir))
}
_ = os.Setenv("GITEA_ROOT", giteaRoot)
return giteaRoot
}
Loading

0 comments on commit a163c53

Please sign in to comment.