Przeglądaj źródła

conf: add unit tests (#5954)

* conf: add tests for utils.go

* conf: add tests for static.go

* mock os/exec

* Run tests on Windows

* appveyor: fix gcc not found

* computed: add unit tests

* log: add unit tests

* log: fix tests on Windows

* conf: add some tests

* Finish adding tests

* Cover more cases

* Add tests for testutil

* Add more tests
ᴜɴᴋɴᴡᴏɴ 4 lat temu
rodzic
commit
8796df8218

+ 4 - 0
appveyor.yml

@@ -11,6 +11,10 @@ build: false
 deploy: false
 
 install:
+  - set PATH=C:\msys64\mingw64\bin;%PATH% # Fix "gcc" not found: https://github.com/appveyor/ci/issues/2613
   - go version
   - go env
   - go build -tags "minwinsvc" -v
+
+test_script:
+  - go test -v -race -cover ./...

+ 2 - 2
internal/conf/computed.go

@@ -99,8 +99,8 @@ var (
 // string when environment variables are not set.
 func HomeDir() string {
 	homeDirOnce.Do(func() {
-		if !IsWindowsRuntime() {
-			homeDir = os.Getenv("HOME")
+		homeDir = os.Getenv("HOME")
+		if homeDir != "" {
 			return
 		}
 

+ 126 - 0
internal/conf/computed_test.go

@@ -0,0 +1,126 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package conf
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"gogs.io/gogs/internal/testutil"
+)
+
+func TestIsProdMode(t *testing.T) {
+	before := App.RunMode
+	defer func() {
+		App.RunMode = before
+	}()
+
+	tests := []struct {
+		mode string
+		want bool
+	}{
+		{mode: "dev", want: false},
+		{mode: "test", want: false},
+
+		{mode: "prod", want: true},
+		{mode: "Prod", want: true},
+		{mode: "PROD", want: true},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			App.RunMode = test.mode
+			assert.Equal(t, test.want, IsProdMode())
+		})
+	}
+}
+
+func TestWorkDirHelper(t *testing.T) {
+	if !testutil.WantHelperProcess() {
+		return
+	}
+
+	fmt.Fprintln(os.Stdout, WorkDir())
+}
+
+func TestWorkDir(t *testing.T) {
+	tests := []struct {
+		env  string
+		want string
+	}{
+		{env: "GOGS_WORK_DIR=/tmp", want: "/tmp"},
+		{env: "", want: WorkDir()},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			out, err := testutil.Exec("TestWorkDirHelper", test.env)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			assert.Equal(t, test.want, out)
+		})
+	}
+}
+
+func TestCustomDirHelper(t *testing.T) {
+	if !testutil.WantHelperProcess() {
+		return
+	}
+
+	fmt.Fprintln(os.Stdout, CustomDir())
+}
+
+func TestCustomDir(t *testing.T) {
+	tests := []struct {
+		env  string
+		want string
+	}{
+		{env: "GOGS_CUSTOM=/tmp", want: "/tmp"},
+		{env: "", want: CustomDir()},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			out, err := testutil.Exec("TestCustomDirHelper", test.env)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			assert.Equal(t, test.want, out)
+		})
+	}
+}
+
+func TestHomeDirHelper(t *testing.T) {
+	if !testutil.WantHelperProcess() {
+		return
+	}
+
+	fmt.Fprintln(os.Stdout, HomeDir())
+}
+
+func TestHomeDir(t *testing.T) {
+	tests := []struct {
+		envs []string
+		want string
+	}{
+		{envs: []string{"HOME=/tmp"}, want: "/tmp"},
+		{envs: []string{`USERPROFILE=C:\Users\Joe`}, want: `C:\Users\Joe`},
+		{envs: []string{`HOMEDRIVE=C:`, `HOMEPATH=\Users\Joe`}, want: `C:\Users\Joe`},
+		{envs: nil, want: ""},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			out, err := testutil.Exec("TestHomeDirHelper", test.envs...)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			assert.Equal(t, test.want, out)
+		})
+	}
+}

+ 1 - 1
internal/conf/conf.go

@@ -353,7 +353,7 @@ func Init(customConf string) error {
 	// ----- I18n settings -----
 	// *************************
 
-	I18n = new(i18n)
+	I18n = new(i18nConf)
 	if err = File.Section("i18n").MapTo(I18n); err != nil {
 		return errors.Wrap(err, "mapping [i18n] section")
 	}

+ 88 - 0
internal/conf/conf_test.go

@@ -0,0 +1,88 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package conf
+
+import (
+	"bytes"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/ini.v1"
+
+	"gogs.io/gogs/internal/testutil"
+)
+
+func TestAsset(t *testing.T) {
+	// Make sure it does not blow up
+	_, err := Asset("conf/app.ini")
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestAssetDir(t *testing.T) {
+	// Make sure it does not blow up
+	_, err := AssetDir("conf")
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestMustAsset(t *testing.T) {
+	// Make sure it does not blow up
+	MustAsset("conf/app.ini")
+}
+
+func TestInit(t *testing.T) {
+	if IsWindowsRuntime() {
+		return
+	}
+
+	ini.PrettyFormat = false
+	defer func() {
+		MustInit("")
+		ini.PrettyFormat = true
+	}()
+
+	assert.Nil(t, Init(filepath.Join("testdata", "custom.ini")))
+
+	cfg := ini.Empty()
+	cfg.NameMapper = ini.SnackCase
+
+	for _, v := range []struct {
+		section string
+		config  interface{}
+	}{
+		{"", &App},
+		{"server", &Server},
+		{"server", &SSH},
+		{"repository", &Repository},
+		{"database", &Database},
+		{"security", &Security},
+		{"email", &Email},
+		{"auth", &Auth},
+		{"user", &User},
+		{"session", &Session},
+		{"attachment", &Attachment},
+		{"time", &Time},
+		{"picture", &Picture},
+		{"mirror", &Mirror},
+		{"i18n", &I18n},
+	} {
+		err := cfg.Section(v.section).ReflectFrom(v.config)
+		if err != nil {
+			t.Fatalf("%s: %v", v.section, err)
+		}
+	}
+
+	buf := new(bytes.Buffer)
+	_, err := cfg.WriteTo(buf)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	testutil.AssertGolden(t, filepath.Join("testdata", "TestInit.golden.ini"), testutil.Update("TestInit"), buf.String())
+}

+ 79 - 43
internal/conf/log.go

@@ -9,28 +9,39 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/pkg/errors"
+	"gopkg.in/ini.v1"
 	log "unknwon.dev/clog/v2"
 )
 
-// Log settings
-var Log struct {
+type loggerConf struct {
+	Buffer int64
+	Config interface{}
+}
+
+type logConf struct {
 	RootPath string
 	Modes    []string
-	Configs  []interface{}
+	Configs  []*loggerConf
 }
 
-// InitLogging initializes the logging service of the application.
-func InitLogging() {
-	Log.RootPath = File.Section("log").Key("ROOT_PATH").MustString(filepath.Join(WorkDir(), "log"))
-
-	// Because we always create a console logger as the primary logger at init time,
-	// we need to remove it in case the user doesn't configure to use it after the
-	// logging service is initalized.
-	hasConsole := false
+// Log settings
+var Log *logConf
+
+// initLogConf returns parsed logging configuration from given INI file.
+// NOTE: Because we always create a console logger as the primary logger at init time,
+// we need to remove it in case the user doesn't configure to use it after the logging
+// service is initalized.
+func initLogConf(cfg *ini.File) (_ *logConf, hasConsole bool, _ error) {
+	rootPath := cfg.Section("log").Key("ROOT_PATH").MustString(filepath.Join(WorkDir(), "log"))
+	modes := strings.Split(cfg.Section("log").Key("MODE").MustString("console"), ",")
+	lc := &logConf{
+		RootPath: ensureAbs(rootPath),
+		Modes:    make([]string, 0, len(modes)),
+		Configs:  make([]*loggerConf, 0, len(modes)),
+	}
 
 	// Iterate over [log.*] sections to initialize individual logger.
-	Log.Modes = strings.Split(File.Section("log").Key("MODE").MustString("console"), ",")
-	Log.Configs = make([]interface{}, 0, len(Log.Modes))
 	levelMappings := map[string]log.Level{
 		"trace": log.LevelTrace,
 		"info":  log.LevelInfo,
@@ -39,43 +50,30 @@ func InitLogging() {
 		"fatal": log.LevelFatal,
 	}
 
-	type config struct {
-		Buffer int64
-		Config interface{}
-	}
-	for _, mode := range Log.Modes {
-		mode = strings.ToLower(strings.TrimSpace(mode))
-		secName := "log." + mode
-		sec, err := File.GetSection(secName)
+	for i := range modes {
+		modes[i] = strings.ToLower(strings.TrimSpace(modes[i]))
+		secName := "log." + modes[i]
+		sec, err := cfg.GetSection(secName)
 		if err != nil {
-			log.Fatal("Missing configuration section [%s] for %q logger", secName, mode)
-			return
+			return nil, hasConsole, errors.Errorf("missing configuration section [%s] for %q logger", secName, modes[i])
 		}
 
 		level := levelMappings[strings.ToLower(sec.Key("LEVEL").MustString("trace"))]
 		buffer := sec.Key("BUFFER_LEN").MustInt64(100)
-		var c *config
-		switch mode {
+		var c *loggerConf
+		switch modes[i] {
 		case log.DefaultConsoleName:
 			hasConsole = true
-			c = &config{
+			c = &loggerConf{
 				Buffer: buffer,
 				Config: log.ConsoleConfig{
 					Level: level,
 				},
 			}
-			err = log.NewConsole(c.Buffer, c.Config)
 
 		case log.DefaultFileName:
-			logPath := filepath.Join(Log.RootPath, "gogs.log")
-			logDir := filepath.Dir(logPath)
-			err = os.MkdirAll(logDir, os.ModePerm)
-			if err != nil {
-				log.Fatal("Failed to create log directory %q: %v", logDir, err)
-				return
-			}
-
-			c = &config{
+			logPath := filepath.Join(lc.RootPath, "gogs.log")
+			c = &loggerConf{
 				Buffer: buffer,
 				Config: log.FileConfig{
 					Level:    level,
@@ -89,20 +87,18 @@ func InitLogging() {
 					},
 				},
 			}
-			err = log.NewFile(c.Buffer, c.Config)
 
 		case log.DefaultSlackName:
-			c = &config{
+			c = &loggerConf{
 				Buffer: buffer,
 				Config: log.SlackConfig{
 					Level: level,
 					URL:   sec.Key("URL").String(),
 				},
 			}
-			err = log.NewSlack(c.Buffer, c.Config)
 
 		case log.DefaultDiscordName:
-			c = &config{
+			c = &loggerConf{
 				Buffer: buffer,
 				Config: log.DiscordConfig{
 					Level:    level,
@@ -110,22 +106,62 @@ func InitLogging() {
 					Username: sec.Key("USERNAME").String(),
 				},
 			}
-			err = log.NewDiscord(c.Buffer, c.Config)
 
 		default:
 			continue
 		}
 
+		lc.Modes = append(lc.Modes, modes[i])
+		lc.Configs = append(lc.Configs, c)
+	}
+
+	return lc, hasConsole, nil
+}
+
+// InitLogging initializes the logging service of the application.
+func InitLogging() {
+	logConf, hasConsole, err := initLogConf(File)
+	if err != nil {
+		log.Fatal("Failed to init logging configuration: %v", err)
+	}
+
+	err = os.MkdirAll(logConf.RootPath, os.ModePerm)
+	if err != nil {
+		log.Fatal("Failed to create log directory: %v", err)
+	}
+
+	for i, mode := range logConf.Modes {
+		c := logConf.Configs[i]
+
+		var err error
+		var level log.Level
+		switch mode {
+		case log.DefaultConsoleName:
+			level = c.Config.(log.ConsoleConfig).Level
+			err = log.NewConsole(c.Buffer, c.Config)
+		case log.DefaultFileName:
+			level = c.Config.(log.FileConfig).Level
+			err = log.NewFile(c.Buffer, c.Config)
+		case log.DefaultSlackName:
+			level = c.Config.(log.SlackConfig).Level
+			err = log.NewSlack(c.Buffer, c.Config)
+		case log.DefaultDiscordName:
+			level = c.Config.(log.DiscordConfig).Level
+			err = log.NewDiscord(c.Buffer, c.Config)
+		default:
+			panic("unreachable")
+		}
+
 		if err != nil {
 			log.Fatal("Failed to init %s logger: %v", mode, err)
 			return
 		}
-
-		Log.Configs = append(Log.Configs, c)
 		log.Trace("Log mode: %s (%s)", strings.Title(mode), strings.Title(strings.ToLower(level.String())))
 	}
 
 	if !hasConsole {
 		log.Remove(log.DefaultConsoleName)
 	}
+
+	Log = logConf
 }

+ 134 - 0
internal/conf/log_test.go

@@ -0,0 +1,134 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package conf
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"gopkg.in/ini.v1"
+	log "unknwon.dev/clog/v2"
+)
+
+func Test_initLogConf(t *testing.T) {
+	t.Run("missing configuration section", func(t *testing.T) {
+		f, err := ini.Load([]byte(`
+[log]
+MODE = console
+`))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		got, hasConsole, err := initLogConf(f)
+		assert.NotNil(t, err)
+		assert.Equal(t, `missing configuration section [log.console] for "console" logger`, err.Error())
+		assert.False(t, hasConsole)
+		assert.Nil(t, got)
+	})
+
+	t.Run("no console logger", func(t *testing.T) {
+		f, err := ini.Load([]byte(`
+[log]
+MODE = file
+
+[log.file]
+`))
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		got, hasConsole, err := initLogConf(f)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.False(t, hasConsole)
+		assert.NotNil(t, got)
+	})
+
+	f, err := ini.Load([]byte(`
+[log]
+ROOT_PATH = log
+MODE = console, file, slack, discord
+BUFFER_LEN = 50
+LEVEL = trace
+
+[log.console]
+BUFFER_LEN = 10
+
+[log.file]
+LEVEL = INFO
+LOG_ROTATE = true
+DAILY_ROTATE = true
+MAX_SIZE_SHIFT = 20
+MAX_LINES = 1000
+MAX_DAYS = 3
+
+[log.slack]
+LEVEL = Warn
+URL = https://slack.com
+
+[log.discord]
+LEVEL = error
+URL = https://discordapp.com
+USERNAME = yoyo
+`))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	got, hasConsole, err := initLogConf(f)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	logConf := &logConf{
+		RootPath: filepath.Join(WorkDir(), "log"),
+		Modes: []string{
+			log.DefaultConsoleName,
+			log.DefaultFileName,
+			log.DefaultSlackName,
+			log.DefaultDiscordName,
+		},
+		Configs: []*loggerConf{
+			{
+				Buffer: 10,
+				Config: log.ConsoleConfig{
+					Level: log.LevelTrace,
+				},
+			}, {
+				Buffer: 50,
+				Config: log.FileConfig{
+					Level:    log.LevelInfo,
+					Filename: filepath.Join(WorkDir(), "log", "gogs.log"),
+					FileRotationConfig: log.FileRotationConfig{
+						Rotate:   true,
+						Daily:    true,
+						MaxSize:  1 << 20,
+						MaxLines: 1000,
+						MaxDays:  3,
+					},
+				},
+			}, {
+				Buffer: 50,
+				Config: log.SlackConfig{
+					Level: log.LevelWarn,
+					URL:   "https://slack.com",
+				},
+			}, {
+				Buffer: 50,
+				Config: log.DiscordConfig{
+					Level:    log.LevelError,
+					URL:      "https://discordapp.com",
+					Username: "yoyo",
+				},
+			},
+		},
+	}
+	assert.True(t, hasConsole)
+	assert.Equal(t, logConf, got)
+}

+ 13 - 11
internal/conf/static.go

@@ -16,12 +16,12 @@ import (
 
 // HasMinWinSvc is whether the application is built with Windows Service support.
 //
-// ⚠️ WARNING: should only be set by "static_minwinsvc.go".
+// ⚠️ WARNING: should only be set by "internal/conf/static_minwinsvc.go".
 var HasMinWinSvc bool
 
 // Build time and commit information.
 //
-// ⚠️ WARNING: should only be set by -ldflags.
+// ⚠️ WARNING: should only be set by "-ldflags".
 var (
 	BuildTime   string
 	BuildCommit string
@@ -35,7 +35,7 @@ var CustomConf string
 var (
 	// Application settings
 	App struct {
-		// ⚠️ WARNING: Should only be set by main package (i.e. "gogs.go").
+		// ⚠️ WARNING: Should only be set by the main package (i.e. "gogs.go").
 		Version string `ini:"-"`
 
 		BrandName string
@@ -288,7 +288,7 @@ var (
 	}
 
 	// I18n settings
-	I18n *i18n
+	I18n *i18nConf
 
 	// Webhook settings
 	Webhook struct {
@@ -349,7 +349,9 @@ var (
 
 	// Git settings
 	Git struct {
-		Version                  string `ini:"-"`
+		// ⚠️ WARNING: Should only be set by "internal/db/repo.go".
+		Version string `ini:"-"`
+
 		DisableDiffHighlight     bool
 		MaxGitDiffLines          int
 		MaxGitDiffLineCharacters int
@@ -408,15 +410,15 @@ var (
 	HasRobotsTxt bool
 )
 
-type i18n struct {
-	Langs     []string `delim:","`
-	Names     []string `delim:","`
-	dateLangs map[string]string
+type i18nConf struct {
+	Langs     []string          `delim:","`
+	Names     []string          `delim:","`
+	dateLangs map[string]string `ini:"-"`
 }
 
 // DateLang transforms standard language locale name to corresponding value in datetime plugin.
-func (i *i18n) DateLang(lang string) string {
-	name, ok := i.dateLangs[lang]
+func (c *i18nConf) DateLang(lang string) string {
+	name, ok := c.dateLangs[lang]
 	if ok {
 		return name
 	}

+ 35 - 0
internal/conf/static_test.go

@@ -0,0 +1,35 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package conf
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_i18n_DateLang(t *testing.T) {
+	c := &i18nConf{
+		dateLangs: map[string]string{
+			"en-US": "en",
+			"zh-CN": "zh",
+		},
+	}
+
+	tests := []struct {
+		lang string
+		want string
+	}{
+		{lang: "en-US", want: "en"},
+		{lang: "zh-CN", want: "zh"},
+
+		{lang: "jp-JP", want: "en"},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			assert.Equal(t, test.want, c.DateLang(test.lang))
+		})
+	}
+}

+ 153 - 0
internal/conf/testdata/TestInit.golden.ini

@@ -0,0 +1,153 @@
+BRAND_NAME=Testing
+RUN_USER=git
+RUN_MODE=test
+APP_NAME=
+
+[server]
+EXTERNAL_URL=http://localhost:3080/
+DOMAIN=localhost
+PROTOCOL=http
+HTTP_ADDR=0.0.0.0
+HTTP_PORT=3000
+CERT_FILE=custom/https/cert.pem
+KEY_FILE=custom/https/key.pem
+TLS_MIN_VERSION=TLS12
+UNIX_SOCKET_PERMISSION=666
+LOCAL_ROOT_URL=http://0.0.0.0:3000/
+OFFLINE_MODE=false
+DISABLE_ROUTER_LOG=true
+ENABLE_GZIP=false
+APP_DATA_PATH=/tmp/data
+LOAD_ASSETS_FROM_DISK=false
+LANDING_URL=/explore
+ROOT_URL=
+LANDING_PAGE=
+DISABLE_SSH=false
+SSH_DOMAIN=localhost
+SSH_PORT=22
+SSH_ROOT_PATH=/tmp
+SSH_KEYGEN_PATH=ssh-keygen
+SSH_KEY_TEST_PATH=/tmp/ssh-key-test
+MINIMUM_KEY_SIZE_CHECK=true
+REWRITE_AUTHORIZED_KEYS_AT_START=false
+START_SSH_SERVER=false
+SSH_LISTEN_HOST=0.0.0.0
+SSH_LISTEN_PORT=22
+SSH_SERVER_CIPHERS=aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,arcfour256,arcfour128
+
+[repository]
+ROOT=/tmp/gogs-repositories
+SCRIPT_TYPE=bash
+ANSI_CHARSET=
+FORCE_PRIVATE=false
+MAX_CREATION_LIMIT=-1
+PREFERRED_LICENSES=Apache License 2.0,MIT License
+DISABLE_HTTP_GIT=false
+ENABLE_LOCAL_PATH_MIGRATION=false
+ENABLE_RAW_FILE_RENDER_MODE=false
+COMMITS_FETCH_CONCURRENCY=0
+
+[repository.editor]
+LINE_WRAP_EXTENSIONS=.txt,.md,.markdown,.mdown,.mkd
+PREVIEWABLE_FILE_MODES=markdown
+
+[repository.upload]
+ENABLED=true
+TEMP_PATH=/tmp/uploads
+ALLOWED_TYPES=
+FILE_MAX_SIZE=3
+MAX_FILES=5
+
+[database]
+TYPE=sqlite
+HOST=127.0.0.1:5432
+NAME=gogs
+USER=gogs
+PASSWORD=12345678
+SSL_MODE=disable
+PATH=/tmp/gogs.db
+DB_TYPE=
+PASSWD=
+
+[security]
+INSTALL_LOCK=false
+SECRET_KEY=`!#@FDEWREWR&*(`
+LOGIN_REMEMBER_DAYS=7
+COOKIE_REMEMBER_NAME=gogs_incredible
+COOKIE_USERNAME=gogs_awesome
+COOKIE_SECURE=false
+ENABLE_LOGIN_STATUS_COOKIE=false
+LOGIN_STATUS_COOKIE_NAME=login_status
+REVERSE_PROXY_AUTHENTICATION_USER=
+
+[email]
+ENABLED=true
+SUBJECT_PREFIX=[Gogs] 
+HOST=smtp.mailgun.org:587
+FROM=noreply@gogs.localhost
+USER=noreply@gogs.localhost
+PASSWORD=87654321
+DISABLE_HELO=false
+HELO_HOSTNAME=
+SKIP_VERIFY=false
+USE_CERTIFICATE=false
+CERT_FILE=custom/email/cert.pem
+KEY_FILE=custom/email/key.pem
+USE_PLAIN_TEXT=false
+ADD_PLAIN_TEXT_ALT=false
+PASSWD=
+
+[auth]
+ACTIVATE_CODE_LIVES=10
+RESET_PASSWORD_CODE_LIVES=10
+REQUIRE_EMAIL_CONFIRMATION=true
+REQUIRE_SIGNIN_VIEW=false
+DISABLE_REGISTRATION=false
+ENABLE_REGISTRATION_CAPTCHA=true
+ENABLE_REVERSE_PROXY_AUTHENTICATION=false
+ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=false
+REVERSE_PROXY_AUTHENTICATION_HEADER=X-FORWARDED-FOR
+ACTIVE_CODE_LIVE_MINUTES=0
+RESET_PASSWD_CODE_LIVE_MINUTES=0
+REGISTER_EMAIL_CONFIRM=false
+ENABLE_CAPTCHA=false
+ENABLE_NOTIFY_MAIL=false
+
+[user]
+ENABLE_EMAIL_NOTIFICATION=true
+
+[session]
+PROVIDER=memory
+PROVIDER_CONFIG=data/sessions
+COOKIE_NAME=i_like_gogs
+COOKIE_SECURE=false
+GC_INTERVAL=10
+MAX_LIFE_TIME=10
+CSRF_COOKIE_NAME=_csrf
+GC_INTERVAL_TIME=0
+SESSION_LIFE_TIME=0
+
+[attachment]
+ENABLED=true
+PATH=/tmp/attachments
+ALLOWED_TYPES=image/jpeg|image/png
+MAX_SIZE=4
+MAX_FILES=5
+
+[time]
+FORMAT=RFC1123
+
+[picture]
+AVATAR_UPLOAD_PATH=/tmp/avatars
+REPOSITORY_AVATAR_UPLOAD_PATH=/tmp/repo-avatars
+GRAVATAR_SOURCE=https://secure.gravatar.com/avatar/
+DISABLE_GRAVATAR=false
+ENABLE_FEDERATED_AVATAR=false
+
+[mirror]
+DEFAULT_INTERVAL=8
+
+[i18n]
+LANGS=en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,gl-ES,uk-UA,en-GB,hu-HU,sk-SK,id-ID,fa-IR,vi-VN,pt-PT
+NAMES=English,简体中文,繁體中文(香港),繁體中文(臺灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,galego,українська,English (United Kingdom),Magyar,Slovenčina,Indonesian,Persian,Vietnamese,Português
+

+ 46 - 0
internal/conf/testdata/custom.ini

@@ -0,0 +1,46 @@
+APP_NAME = Testing
+RUN_MODE = test
+
+[server]
+ROOT_URL = http://localhost:3080/
+APP_DATA_PATH = /tmp/data
+SSH_ROOT_PATH = /tmp
+SSH_KEY_TEST_PATH = /tmp/ssh-key-test
+MINIMUM_KEY_SIZE_CHECK = true
+LANDING_PAGE = explore
+
+[repository]
+ROOT = /tmp/gogs-repositories
+
+[repository.upload]
+TEMP_PATH = /tmp/uploads
+
+[database]
+DB_TYPE = sqlite
+PASSWD = 12345678
+PATH = /tmp/gogs.db
+
+[security]
+REVERSE_PROXY_AUTHENTICATION_USER=X-FORWARDED-FOR
+
+[email]
+ENABLED = true
+PASSWD = 87654321
+
+[auth]
+ACTIVE_CODE_LIVE_MINUTES = 10
+RESET_PASSWD_CODE_LIVE_MINUTES = 10
+REGISTER_EMAIL_CONFIRM = true
+ENABLE_CAPTCHA = true
+ENABLE_NOTIFY_MAIL = true
+
+[session]
+GC_INTERVAL_TIME = 10
+SESSION_LIFE_TIME = 10
+
+[attachment]
+PATH = /tmp/attachments
+
+[picture]
+AVATAR_UPLOAD_PATH = /tmp/avatars
+REPOSITORY_AVATAR_UPLOAD_PATH = /tmp/repo-avatars

+ 8 - 4
internal/conf/utils.go

@@ -14,6 +14,13 @@ import (
 	"gogs.io/gogs/internal/process"
 )
 
+// cleanUpOpenSSHVersion cleans up the raw output of "ssh -V" and returns a clean version string.
+func cleanUpOpenSSHVersion(raw string) string {
+	v := strings.TrimRight(strings.Fields(raw)[0], ",1234567890")
+	v = strings.TrimSuffix(strings.TrimPrefix(v, "OpenSSH_"), "p")
+	return v
+}
+
 // openSSHVersion returns string representation of OpenSSH version via command "ssh -V".
 func openSSHVersion() (string, error) {
 	// NOTE: Somehow the version is printed to stderr.
@@ -22,10 +29,7 @@ func openSSHVersion() (string, error) {
 		return "", errors.Wrap(err, stderr)
 	}
 
-	// Trim unused information, see https://github.com/gogs/gogs/issues/4507#issuecomment-305150441.
-	v := strings.TrimRight(strings.Fields(stderr)[0], ",1234567890")
-	v = strings.TrimSuffix(strings.TrimPrefix(v, "OpenSSH_"), "p")
-	return v, nil
+	return cleanUpOpenSSHVersion(stderr), nil
 }
 
 // ensureAbs prepends the WorkDir to the given path if it is not an absolute path.

+ 57 - 0
internal/conf/utils_test.go

@@ -0,0 +1,57 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package conf
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_cleanUpOpenSSHVersion(t *testing.T) {
+	tests := []struct {
+		raw  string
+		want string
+	}{
+		{
+			raw:  "OpenSSH_7.4p1 Ubuntu-10, OpenSSL 1.0.2g 1 Mar 2016",
+			want: "7.4",
+		}, {
+			raw:  "OpenSSH_5.3p1, OpenSSL 1.0.1e-fips 11 Feb 2013",
+			want: "5.3",
+		}, {
+			raw:  "OpenSSH_4.3p2, OpenSSL 0.9.8e-fips-rhel5 01 Jul 2008",
+			want: "4.3",
+		},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			assert.Equal(t, test.want, cleanUpOpenSSHVersion(test.raw))
+		})
+	}
+}
+
+func Test_ensureAbs(t *testing.T) {
+	wd := WorkDir()
+
+	tests := []struct {
+		path string
+		want string
+	}{
+		{
+			path: "data/avatars",
+			want: filepath.Join(wd, "data", "avatars"),
+		}, {
+			path: wd,
+			want: wd,
+		},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			assert.Equal(t, test.want, ensureAbs(test.path))
+		})
+	}
+}

+ 3 - 3
internal/osutil/osutil.go

@@ -25,9 +25,9 @@ func IsExist(path string) bool {
 
 // CurrentUsername returns the current system user via environment variables.
 func CurrentUsername() string {
-	curUserName := os.Getenv("USER")
-	if len(curUserName) > 0 {
-		return curUserName
+	username := os.Getenv("USER")
+	if len(username) > 0 {
+		return username
 	}
 
 	return os.Getenv("USERNAME")

+ 5 - 0
internal/osutil/osutil_test.go

@@ -55,3 +55,8 @@ func TestIsExist(t *testing.T) {
 		})
 	}
 }
+
+func TestCurrentUsername(t *testing.T) {
+	// Make sure it does not blow up
+	CurrentUsername()
+}

+ 46 - 0
internal/testutil/exec.go

@@ -0,0 +1,46 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package testutil
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+// Exec executes "go test" on given helper with supplied environment variables.
+// It is useful to mock "os/exec" functions in tests. When succeeded, it returns
+// the result produced by the test helper.
+// The test helper should:
+//     1. Use WantHelperProcess function to determine if it is being called in helper mode.
+//     2. Call fmt.Fprintln(os.Stdout, ...) to print results for the main test to collect.
+func Exec(helper string, envs ...string) (string, error) {
+	cmd := exec.Command(os.Args[0], "-test.run="+helper, "--")
+	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+	cmd.Env = append(cmd.Env, envs...)
+	out, err := cmd.CombinedOutput()
+	str := string(out)
+	if err != nil {
+		return "", fmt.Errorf("%v - %s", err, str)
+	}
+
+	if strings.Contains(str, "no tests to run") {
+		return "", errors.New("no tests to run")
+	} else if !strings.Contains(str, "PASS") {
+		return "", errors.New(str)
+	}
+
+	// Collect helper result
+	result := str[:strings.Index(str, "PASS")]
+	result = strings.TrimSpace(result)
+	return result, nil
+}
+
+// WantHelperProcess returns true if current process is in helper mode.
+func WantHelperProcess() bool {
+	return os.Getenv("GO_WANT_HELPER_PROCESS") == "1"
+}

+ 55 - 0
internal/testutil/exec_test.go

@@ -0,0 +1,55 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package testutil
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestExecHelper(t *testing.T) {
+	if !WantHelperProcess() {
+		return
+	}
+
+	if os.Getenv("PASS") != "1" {
+		fmt.Fprintln(os.Stdout, "tests failed")
+		os.Exit(1)
+	}
+
+	fmt.Fprintln(os.Stdout, "tests succeed")
+}
+
+func TestExec(t *testing.T) {
+	tests := []struct {
+		helper string
+		env    string
+		expOut string
+		expErr error
+	}{
+		{
+			helper: "NoTestsToRun",
+			expErr: errors.New("no tests to run"),
+		}, {
+			helper: "TestExecHelper",
+			expErr: errors.New("exit status 1 - tests failed\n"),
+		}, {
+			helper: "TestExecHelper",
+			env:    "PASS=1",
+			expOut: "tests succeed",
+		},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			out, err := Exec(test.helper, test.env)
+			assert.Equal(t, test.expErr, err)
+			assert.Equal(t, test.expOut, out)
+		})
+	}
+}

+ 63 - 0
internal/testutil/golden.go

@@ -0,0 +1,63 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package testutil
+
+import (
+	"encoding/json"
+	"flag"
+	"io/ioutil"
+	"regexp"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var updateRegex = flag.String("update", "", "Update testdata of tests matching the given regex")
+
+// Update returns true if update regex mathces given test name.
+func Update(name string) bool {
+	if updateRegex == nil || *updateRegex == "" {
+		return false
+	}
+	return regexp.MustCompile(*updateRegex).MatchString(name)
+}
+
+// AssertGolden compares what's got and what's in the golden file. It updates
+// the golden file on-demand.
+func AssertGolden(t testing.TB, path string, update bool, got interface{}) {
+	t.Helper()
+
+	data := marshal(t, got)
+
+	if update {
+		if err := ioutil.WriteFile(path, data, 0640); err != nil {
+			t.Fatalf("update golden file %q: %s", path, err)
+		}
+	}
+
+	golden, err := ioutil.ReadFile(path)
+	if err != nil {
+		t.Fatalf("read golden file %q: %s", path, err)
+	}
+
+	assert.Equal(t, string(golden), string(data))
+}
+
+func marshal(t testing.TB, v interface{}) []byte {
+	t.Helper()
+
+	switch v2 := v.(type) {
+	case string:
+		return []byte(v2)
+	case []byte:
+		return v2
+	default:
+		data, err := json.MarshalIndent(v, "", "  ")
+		if err != nil {
+			t.Fatal(err)
+		}
+		return data
+	}
+}

+ 52 - 0
internal/testutil/golden_test.go

@@ -0,0 +1,52 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package testutil
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestUpdate(t *testing.T) {
+	before := updateRegex
+	defer func() {
+		updateRegex = before
+	}()
+
+	t.Run("no flag", func(t *testing.T) {
+		updateRegex = nil
+		assert.False(t, Update("TestUpdate"))
+	})
+
+	tests := []struct {
+		regex string
+		name  string
+		want  bool
+	}{
+		{regex: "", name: "TestUpdate", want: false},
+		{regex: "TestNotFound", name: "TestUpdate", want: false},
+
+		{regex: ".*", name: "TestUpdate", want: true},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			updateRegex = &test.regex
+			assert.Equal(t, test.want, Update(test.name))
+		})
+	}
+}
+
+func TestAssertGolden(t *testing.T) {
+	// Make sure it does not blow up
+	AssertGolden(t, filepath.Join("testdata", "golden"), false, "{\n  \"Message\": \"This is a golden file.\"\n}")
+	AssertGolden(t, filepath.Join("testdata", "golden"), false, []byte("{\n  \"Message\": \"This is a golden file.\"\n}"))
+
+	type T struct {
+		Message string
+	}
+	AssertGolden(t, filepath.Join("testdata", "golden"), false, T{"This is a golden file."})
+}

+ 3 - 0
internal/testutil/testdata/golden

@@ -0,0 +1,3 @@
+{
+  "Message": "This is a golden file."
+}