~ttt/minifluxlite

f7b7b63e3f30b4d855a26d550ddf726116c65846 — Frédéric Guillot 4 years ago bb720c8
Add optional config file parser in addition to environment variables
M cli/cli.go => cli/cli.go +24 -1
@@ 24,11 24,14 @@ const (
	flagResetPasswordHelp   = "Reset user password"
	flagResetFeedErrorsHelp = "Clear all feed errors for all users"
	flagDebugModeHelp       = "Show debug logs"
	flagConfigFileHelp      = "Load configuration file"
	flagConfigDumpHelp      = "Print parsed configuration values"
)

// Parse parses command line arguments.
func Parse() {
	var (
		err                 error
		flagInfo            bool
		flagVersion         bool
		flagMigrate         bool


@@ 37,6 40,8 @@ func Parse() {
		flagResetPassword   bool
		flagResetFeedErrors bool
		flagDebugMode       bool
		flagConfigFile      string
		flagConfigDump      bool
	)

	flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)


@@ 49,12 54,30 @@ func Parse() {
	flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
	flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
	flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
	flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
	flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
	flag.BoolVar(&flagConfigDump, "config-dump", false, flagConfigDumpHelp)
	flag.Parse()

	if err := config.ParseConfig(); err != nil {
	cfg := config.NewParser()

	if flagConfigFile != "" {
		config.Opts, err = cfg.ParseFile(flagConfigFile)
		if err != nil {
			logger.Fatal("%v", err)
		}
	}

	config.Opts, err = cfg.ParseEnvironmentVariables()
	if err != nil {
		logger.Fatal("%v", err)
	}

	if flagConfigDump {
		fmt.Print(config.Opts)
		return
	}

	if flagDebugMode || config.Opts.HasDebugMode() {
		logger.EnableDebug()
	}

M config/config.go => config/config.go +1 -7
@@ 4,11 4,5 @@

package config // import "miniflux.app/config"

// Opts contains configuration options after parsing.
// Opts holds parsed configuration options.
var Opts *Options

// ParseConfig parses configuration options.
func ParseConfig() (err error) {
	Opts, err = parse()
	return err
}

M config/config_test.go => config/config_test.go +244 -127
@@ 5,6 5,7 @@
package config // import "miniflux.app/config"

import (
	"io/ioutil"
	"os"
	"testing"
)


@@ 13,9 14,10 @@ func TestDebugModeOn(t *testing.T) {
	os.Clearenv()
	os.Setenv("DEBUG", "1")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	if !opts.HasDebugMode() {


@@ 26,9 28,10 @@ func TestDebugModeOn(t *testing.T) {
func TestDebugModeOff(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	if opts.HasDebugMode() {


@@ 40,9 43,10 @@ func TestCustomBaseURL(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "http://example.org")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	if opts.BaseURL() != "http://example.org" {


@@ 62,9 66,10 @@ func TestCustomBaseURLWithTrailingSlash(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "http://example.org/folder/")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	if opts.BaseURL() != "http://example.org/folder" {


@@ 84,7 89,7 @@ func TestBaseURLWithoutScheme(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "example.org/folder/")

	_, err := parse()
	_, err := NewParser().ParseEnvironmentVariables()
	if err == nil {
		t.Fatalf(`Parsing must fail`)
	}


@@ 94,7 99,7 @@ func TestBaseURLWithInvalidScheme(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "ftp://example.org/folder/")

	_, err := parse()
	_, err := NewParser().ParseEnvironmentVariables()
	if err == nil {
		t.Fatalf(`Parsing must fail`)
	}


@@ 104,7 109,7 @@ func TestInvalidBaseURL(t *testing.T) {
	os.Clearenv()
	os.Setenv("BASE_URL", "http://example|org")

	_, err := parse()
	_, err := NewParser().ParseEnvironmentVariables()
	if err == nil {
		t.Fatalf(`Parsing must fail`)
	}


@@ 113,9 118,10 @@ func TestInvalidBaseURL(t *testing.T) {
func TestDefaultBaseURL(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	if opts.BaseURL() != defaultBaseURL {


@@ 135,41 141,52 @@ func TestDatabaseURL(t *testing.T) {
	os.Clearenv()
	os.Setenv("DATABASE_URL", "foobar")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "foobar"
	result := opts.DatabaseURL()

	if result != expected {
		t.Fatalf(`Unexpected DATABASE_URL value, got %q instead of %q`, result, expected)
		t.Errorf(`Unexpected DATABASE_URL value, got %q instead of %q`, result, expected)
	}

	if opts.IsDefaultDatabaseURL() {
		t.Errorf(`This is not the default database URL and it should returns false`)
	}
}

func TestDefaultDatabaseURLValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultDatabaseURL
	result := opts.DatabaseURL()

	if result != expected {
		t.Fatalf(`Unexpected DATABASE_URL value, got %q instead of %q`, result, expected)
		t.Errorf(`Unexpected DATABASE_URL value, got %q instead of %q`, result, expected)
	}

	if !opts.IsDefaultDatabaseURL() {
		t.Errorf(`This is the default database URL and it should returns true`)
	}
}

func TestDefaultDatabaseMaxConnsValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultDatabaseMaxConns


@@ 184,9 201,10 @@ func TestDatabaseMaxConns(t *testing.T) {
	os.Clearenv()
	os.Setenv("DATABASE_MAX_CONNS", "42")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := 42


@@ 200,9 218,10 @@ func TestDatabaseMaxConns(t *testing.T) {
func TestDefaultDatabaseMinConnsValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultDatabaseMinConns


@@ 217,9 236,10 @@ func TestDatabaseMinConns(t *testing.T) {
	os.Clearenv()
	os.Setenv("DATABASE_MIN_CONNS", "42")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := 42


@@ 234,9 254,10 @@ func TestListenAddr(t *testing.T) {
	os.Clearenv()
	os.Setenv("LISTEN_ADDR", "foobar")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "foobar"


@@ 252,9 273,10 @@ func TestListenAddrWithPortDefined(t *testing.T) {
	os.Setenv("PORT", "3000")
	os.Setenv("LISTEN_ADDR", "foobar")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := ":3000"


@@ 268,9 290,10 @@ func TestListenAddrWithPortDefined(t *testing.T) {
func TestDefaultListenAddrValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultListenAddr


@@ 285,9 308,10 @@ func TestCertFile(t *testing.T) {
	os.Clearenv()
	os.Setenv("CERT_FILE", "foobar")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "foobar"


@@ 301,9 325,10 @@ func TestCertFile(t *testing.T) {
func TestDefaultCertFileValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultCertFile


@@ 318,9 343,10 @@ func TestKeyFile(t *testing.T) {
	os.Clearenv()
	os.Setenv("KEY_FILE", "foobar")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "foobar"


@@ 334,9 360,10 @@ func TestKeyFile(t *testing.T) {
func TestDefaultKeyFileValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultKeyFile


@@ 351,9 378,10 @@ func TestCertDomain(t *testing.T) {
	os.Clearenv()
	os.Setenv("CERT_DOMAIN", "example.org")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "example.org"


@@ 367,9 395,10 @@ func TestCertDomain(t *testing.T) {
func TestDefaultCertDomainValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultCertDomain


@@ 384,9 413,10 @@ func TestCertCache(t *testing.T) {
	os.Clearenv()
	os.Setenv("CERT_CACHE", "foobar")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "foobar"


@@ 400,9 430,10 @@ func TestCertCache(t *testing.T) {
func TestDefaultCertCacheValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultCertCache


@@ 416,9 447,10 @@ func TestDefaultCertCacheValue(t *testing.T) {
func TestDefaultCleanupFrequencyValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultCleanupFrequency


@@ 433,9 465,10 @@ func TestCleanupFrequency(t *testing.T) {
	os.Clearenv()
	os.Setenv("CLEANUP_FREQUENCY", "42")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := 42


@@ 449,9 482,10 @@ func TestCleanupFrequency(t *testing.T) {
func TestDefaultWorkerPoolSizeValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultWorkerPoolSize


@@ 466,9 500,10 @@ func TestWorkerPoolSize(t *testing.T) {
	os.Clearenv()
	os.Setenv("WORKER_POOL_SIZE", "42")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := 42


@@ 482,9 517,10 @@ func TestWorkerPoolSize(t *testing.T) {
func TestDefautPollingFrequencyValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultPollingFrequency


@@ 499,9 535,10 @@ func TestPollingFrequency(t *testing.T) {
	os.Clearenv()
	os.Setenv("POLLING_FREQUENCY", "42")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := 42


@@ 515,9 552,10 @@ func TestPollingFrequency(t *testing.T) {
func TestDefaultBatchSizeValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultBatchSize


@@ 532,9 570,10 @@ func TestBatchSize(t *testing.T) {
	os.Clearenv()
	os.Setenv("BATCH_SIZE", "42")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := 42


@@ 548,9 587,10 @@ func TestBatchSize(t *testing.T) {
func TestOAuth2UserCreationWhenUnset(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := false


@@ 565,9 605,10 @@ func TestOAuth2UserCreationAdmin(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_USER_CREATION", "1")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := true


@@ 582,9 623,10 @@ func TestOAuth2ClientID(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_CLIENT_ID", "foobar")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "foobar"


@@ 598,9 640,10 @@ func TestOAuth2ClientID(t *testing.T) {
func TestDefaultOAuth2ClientIDValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultOAuth2ClientID


@@ 615,9 658,10 @@ func TestOAuth2ClientSecret(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_CLIENT_SECRET", "secret")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "secret"


@@ 631,9 675,10 @@ func TestOAuth2ClientSecret(t *testing.T) {
func TestDefaultOAuth2ClientSecretValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultOAuth2ClientSecret


@@ 648,9 693,10 @@ func TestOAuth2RedirectURL(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_REDIRECT_URL", "http://example.org")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "http://example.org"


@@ 664,9 710,10 @@ func TestOAuth2RedirectURL(t *testing.T) {
func TestDefaultOAuth2RedirectURLValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultOAuth2RedirectURL


@@ 681,9 728,10 @@ func TestOAuth2Provider(t *testing.T) {
	os.Clearenv()
	os.Setenv("OAUTH2_PROVIDER", "google")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "google"


@@ 697,9 745,10 @@ func TestOAuth2Provider(t *testing.T) {
func TestDefaultOAuth2ProviderValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultOAuth2Provider


@@ 713,9 762,10 @@ func TestDefaultOAuth2ProviderValue(t *testing.T) {
func TestHSTSWhenUnset(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := true


@@ 730,9 780,10 @@ func TestHSTS(t *testing.T) {
	os.Clearenv()
	os.Setenv("DISABLE_HSTS", "1")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := false


@@ 746,9 797,10 @@ func TestHSTS(t *testing.T) {
func TestDisableHTTPServiceWhenUnset(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := true


@@ 763,9 815,10 @@ func TestDisableHTTPService(t *testing.T) {
	os.Clearenv()
	os.Setenv("DISABLE_HTTP_SERVICE", "1")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := false


@@ 779,9 832,10 @@ func TestDisableHTTPService(t *testing.T) {
func TestDisableSchedulerServiceWhenUnset(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := true


@@ 796,9 850,10 @@ func TestDisableSchedulerService(t *testing.T) {
	os.Clearenv()
	os.Setenv("DISABLE_SCHEDULER_SERVICE", "1")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := false


@@ 813,9 868,10 @@ func TestArchiveReadDays(t *testing.T) {
	os.Clearenv()
	os.Setenv("ARCHIVE_READ_DAYS", "7")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := 7


@@ 829,9 885,10 @@ func TestArchiveReadDays(t *testing.T) {
func TestRunMigrationsWhenUnset(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := false


@@ 846,9 903,10 @@ func TestRunMigrations(t *testing.T) {
	os.Clearenv()
	os.Setenv("RUN_MIGRATIONS", "yes")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := true


@@ 862,9 920,10 @@ func TestRunMigrations(t *testing.T) {
func TestCreateAdminWhenUnset(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := false


@@ 879,9 938,10 @@ func TestCreateAdmin(t *testing.T) {
	os.Clearenv()
	os.Setenv("CREATE_ADMIN", "true")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := true


@@ 896,9 956,10 @@ func TestPocketConsumerKeyFromEnvVariable(t *testing.T) {
	os.Clearenv()
	os.Setenv("POCKET_CONSUMER_KEY", "something")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "something"


@@ 912,9 973,10 @@ func TestPocketConsumerKeyFromEnvVariable(t *testing.T) {
func TestPocketConsumerKeyFromUserPrefs(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "default"


@@ 929,9 991,10 @@ func TestProxyImages(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "all")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := "all"


@@ 945,9 1008,10 @@ func TestProxyImages(t *testing.T) {
func TestDefaultProxyImagesValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultProxyImages


@@ 961,9 1025,10 @@ func TestDefaultProxyImagesValue(t *testing.T) {
func TestHTTPSOff(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	if opts.HTTPS {


@@ 975,9 1040,10 @@ func TestHTTPSOn(t *testing.T) {
	os.Clearenv()
	os.Setenv("HTTPS", "on")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	if !opts.HTTPS {


@@ 989,9 1055,10 @@ func TestHTTPClientTimeout(t *testing.T) {
	os.Clearenv()
	os.Setenv("HTTP_CLIENT_TIMEOUT", "42")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := 42


@@ 1005,9 1072,10 @@ func TestHTTPClientTimeout(t *testing.T) {
func TestDefaultHTTPClientTimeoutValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := defaultHTTPClientTimeout


@@ 1022,9 1090,10 @@ func TestHTTPClientMaxBodySize(t *testing.T) {
	os.Clearenv()
	os.Setenv("HTTP_CLIENT_MAX_BODY_SIZE", "42")

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := int64(42 * 1024 * 1024)


@@ 1038,9 1107,10 @@ func TestHTTPClientMaxBodySize(t *testing.T) {
func TestDefaultHTTPClientMaxBodySizeValue(t *testing.T) {
	os.Clearenv()

	opts, err := parse()
	parser := NewParser()
	opts, err := parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %q`, err)
		t.Fatalf(`Parsing failure: %v`, err)
	}

	expected := int64(defaultHTTPClientMaxBodySize * 1024 * 1024)


@@ 1050,3 1120,50 @@ func TestDefaultHTTPClientMaxBodySizeValue(t *testing.T) {
		t.Fatalf(`Unexpected HTTP_CLIENT_MAX_BODY_SIZE value, got %d instead of %d`, result, expected)
	}
}

func TestParseConfigFile(t *testing.T) {
	content := []byte(`
 # This is a comment

DEBUG = yes

 POCKET_CONSUMER_KEY= >#1234

Invalid text
`)

	tmpfile, err := ioutil.TempFile(".", "miniflux.*.unit_test.conf")
	if err != nil {
		t.Fatal(err)
	}

	if _, err := tmpfile.Write(content); err != nil {
		t.Fatal(err)
	}

	os.Clearenv()

	parser := NewParser()
	opts, err := parser.ParseFile(tmpfile.Name())
	if err != nil {
		t.Errorf(`Parsing failure: %v`, err)
	}

	if opts.HasDebugMode() != true {
		t.Errorf(`Unexpected debug mode value, got "%v"`, opts.HasDebugMode())
	}

	expected := ">#1234"
	result := opts.PocketConsumerKey("default")
	if result != expected {
		t.Errorf(`Unexpected POCKET_CONSUMER_KEY value, got %q instead of %q`, result, expected)
	}

	if err := tmpfile.Close(); err != nil {
		t.Fatal(err)
	}

	if err := os.Remove(tmpfile.Name()); err != nil {
		t.Fatal(err)
	}
}

M config/options.go => config/options.go +91 -0
@@ 4,11 4,24 @@

package config // import "miniflux.app/config"

import (
	"fmt"
	"strings"
)

const (
	defaultHTTPS                 = false
	defaultHSTS                  = true
	defaultHTTPService           = true
	defaultSchedulerService      = true
	defaultDebug                 = false
	defaultBaseURL               = "http://localhost"
	defaultRootURL               = "http://localhost"
	defaultBasePath              = ""
	defaultWorkerPoolSize        = 5
	defaultPollingFrequency      = 60
	defaultBatchSize             = 10
	defaultRunMigrations         = false
	defaultDatabaseURL           = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
	defaultDatabaseMaxConns      = 20
	defaultDatabaseMinConns      = 1


@@ 20,10 33,13 @@ const (
	defaultCertCache             = "/tmp/cert_cache"
	defaultCleanupFrequency      = 24
	defaultProxyImages           = "http-only"
	defaultCreateAdmin           = false
	defaultOAuth2UserCreation    = false
	defaultOAuth2ClientID        = ""
	defaultOAuth2ClientSecret    = ""
	defaultOAuth2RedirectURL     = ""
	defaultOAuth2Provider        = ""
	defaultPocketConsumerKey     = ""
	defaultHTTPClientTimeout     = 20
	defaultHTTPClientMaxBodySize = 15
)


@@ 64,6 80,44 @@ type Options struct {
	httpClientMaxBodySize     int64
}

// NewOptions returns Options with default values.
func NewOptions() *Options {
	return &Options{
		HTTPS:                     defaultHTTPS,
		hsts:                      defaultHSTS,
		httpService:               defaultHTTPService,
		schedulerService:          defaultSchedulerService,
		debug:                     defaultDebug,
		baseURL:                   defaultBaseURL,
		rootURL:                   defaultRootURL,
		basePath:                  defaultBasePath,
		databaseURL:               defaultDatabaseURL,
		databaseMaxConns:          defaultDatabaseMaxConns,
		databaseMinConns:          defaultDatabaseMinConns,
		runMigrations:             defaultRunMigrations,
		listenAddr:                defaultListenAddr,
		certFile:                  defaultCertFile,
		certDomain:                defaultCertDomain,
		certCache:                 defaultCertCache,
		certKeyFile:               defaultKeyFile,
		cleanupFrequency:          defaultCleanupFrequency,
		archiveReadDays:           defaultArchiveReadDays,
		pollingFrequency:          defaultPollingFrequency,
		batchSize:                 defaultBatchSize,
		workerPoolSize:            defaultWorkerPoolSize,
		createAdmin:               defaultCreateAdmin,
		proxyImages:               defaultProxyImages,
		oauth2UserCreationAllowed: defaultOAuth2UserCreation,
		oauth2ClientID:            defaultOAuth2ClientID,
		oauth2ClientSecret:        defaultOAuth2ClientSecret,
		oauth2RedirectURL:         defaultOAuth2RedirectURL,
		oauth2Provider:            defaultOAuth2Provider,
		pocketConsumerKey:         defaultPocketConsumerKey,
		httpClientTimeout:         defaultHTTPClientTimeout,
		httpClientMaxBodySize:     defaultHTTPClientMaxBodySize * 1024 * 1024,
	}
}

// HasDebugMode returns true if debug mode is enabled.
func (o *Options) HasDebugMode() bool {
	return o.debug


@@ 226,3 280,40 @@ func (o *Options) HTTPClientTimeout() int {
func (o *Options) HTTPClientMaxBodySize() int64 {
	return o.httpClientMaxBodySize
}

func (o *Options) String() string {
	var builder strings.Builder
	builder.WriteString(fmt.Sprintf("DEBUG: %v\n", o.debug))
	builder.WriteString(fmt.Sprintf("HTTP_SERVICE: %v\n", o.httpService))
	builder.WriteString(fmt.Sprintf("SCHEDULER_SERVICE: %v\n", o.schedulerService))
	builder.WriteString(fmt.Sprintf("HTTPS: %v\n", o.HTTPS))
	builder.WriteString(fmt.Sprintf("HSTS: %v\n", o.hsts))
	builder.WriteString(fmt.Sprintf("BASE_URL: %v\n", o.baseURL))
	builder.WriteString(fmt.Sprintf("ROOT_URL: %v\n", o.rootURL))
	builder.WriteString(fmt.Sprintf("BASE_PATH: %v\n", o.basePath))
	builder.WriteString(fmt.Sprintf("LISTEN_ADDR: %v\n", o.listenAddr))
	builder.WriteString(fmt.Sprintf("DATABASE_URL: %v\n", o.databaseURL))
	builder.WriteString(fmt.Sprintf("DATABASE_MAX_CONNS: %v\n", o.databaseMaxConns))
	builder.WriteString(fmt.Sprintf("DATABASE_MIN_CONNS: %v\n", o.databaseMinConns))
	builder.WriteString(fmt.Sprintf("RUN_MIGRATIONS: %v\n", o.runMigrations))
	builder.WriteString(fmt.Sprintf("CERT_FILE: %v\n", o.certFile))
	builder.WriteString(fmt.Sprintf("KEY_FILE: %v\n", o.certKeyFile))
	builder.WriteString(fmt.Sprintf("CERT_DOMAIN: %v\n", o.certDomain))
	builder.WriteString(fmt.Sprintf("CERT_CACHE: %v\n", o.certCache))
	builder.WriteString(fmt.Sprintf("CLEANUP_FREQUENCY: %v\n", o.cleanupFrequency))
	builder.WriteString(fmt.Sprintf("WORKER_POOL_SIZE: %v\n", o.workerPoolSize))
	builder.WriteString(fmt.Sprintf("POLLING_FREQUENCY: %v\n", o.pollingFrequency))
	builder.WriteString(fmt.Sprintf("BATCH_SIZE: %v\n", o.batchSize))
	builder.WriteString(fmt.Sprintf("ARCHIVE_READ_DAYS: %v\n", o.archiveReadDays))
	builder.WriteString(fmt.Sprintf("PROXY_IMAGES: %v\n", o.proxyImages))
	builder.WriteString(fmt.Sprintf("CREATE_ADMIN: %v\n", o.createAdmin))
	builder.WriteString(fmt.Sprintf("POCKET_CONSUMER_KEY: %v\n", o.pocketConsumerKey))
	builder.WriteString(fmt.Sprintf("OAUTH2_USER_CREATION: %v\n", o.oauth2UserCreationAllowed))
	builder.WriteString(fmt.Sprintf("OAUTH2_CLIENT_ID: %v\n", o.oauth2ClientID))
	builder.WriteString(fmt.Sprintf("OAUTH2_CLIENT_SECRET: %v\n", o.oauth2ClientSecret))
	builder.WriteString(fmt.Sprintf("OAUTH2_REDIRECT_URL: %v\n", o.oauth2RedirectURL))
	builder.WriteString(fmt.Sprintf("OAUTH2_PROVIDER: %v\n", o.oauth2Provider))
	builder.WriteString(fmt.Sprintf("HTTP_CLIENT_TIMEOUT: %v\n", o.httpClientTimeout))
	builder.WriteString(fmt.Sprintf("HTTP_CLIENT_MAX_BODY_SIZE: %v\n", o.httpClientMaxBodySize))
	return builder.String()
}

M config/parser.go => config/parser.go +146 -68
@@ 5,113 5,184 @@
package config // import "miniflux.app/config"

import (
	"bufio"
	"errors"
	"fmt"
	"net/url"
	"io"
	url_parser "net/url"
	"os"
	"strconv"
	"strings"
)

func parse() (opts *Options, err error) {
	opts = &Options{}
	opts.baseURL, opts.rootURL, opts.basePath, err = parseBaseURL()
// Parser handles configuration parsing.
type Parser struct {
	opts *Options
}

// NewParser returns a new Parser.
func NewParser() *Parser {
	return &Parser{
		opts: NewOptions(),
	}
}

// ParseEnvironmentVariables loads configuration values from environment variables.
func (p *Parser) ParseEnvironmentVariables() (*Options, error) {
	err := p.parseLines(os.Environ())
	if err != nil {
		return nil, err
	}
	return p.opts, nil
}

	opts.debug = getBooleanValue("DEBUG")
	opts.listenAddr = parseListenAddr()

	opts.databaseURL = getStringValue("DATABASE_URL", defaultDatabaseURL)
	opts.databaseMaxConns = getIntValue("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
	opts.databaseMinConns = getIntValue("DATABASE_MIN_CONNS", defaultDatabaseMinConns)
	opts.runMigrations = getBooleanValue("RUN_MIGRATIONS")

	opts.hsts = !getBooleanValue("DISABLE_HSTS")
	opts.HTTPS = getBooleanValue("HTTPS")

	opts.schedulerService = !getBooleanValue("DISABLE_SCHEDULER_SERVICE")
	opts.httpService = !getBooleanValue("DISABLE_HTTP_SERVICE")

	opts.certFile = getStringValue("CERT_FILE", defaultCertFile)
	opts.certKeyFile = getStringValue("KEY_FILE", defaultKeyFile)
	opts.certDomain = getStringValue("CERT_DOMAIN", defaultCertDomain)
	opts.certCache = getStringValue("CERT_CACHE", defaultCertCache)
// ParseFile loads configuration values from a local file.
func (p *Parser) ParseFile(filename string) (*Options, error) {
	fp, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer fp.Close()

	opts.cleanupFrequency = getIntValue("CLEANUP_FREQUENCY", defaultCleanupFrequency)
	opts.workerPoolSize = getIntValue("WORKER_POOL_SIZE", defaultWorkerPoolSize)
	opts.pollingFrequency = getIntValue("POLLING_FREQUENCY", defaultPollingFrequency)
	opts.batchSize = getIntValue("BATCH_SIZE", defaultBatchSize)
	opts.archiveReadDays = getIntValue("ARCHIVE_READ_DAYS", defaultArchiveReadDays)
	opts.proxyImages = getStringValue("PROXY_IMAGES", defaultProxyImages)
	opts.createAdmin = getBooleanValue("CREATE_ADMIN")
	opts.pocketConsumerKey = getStringValue("POCKET_CONSUMER_KEY", "")
	err = p.parseLines(p.parseFileContent(fp))
	if err != nil {
		return nil, err
	}
	return p.opts, nil
}

	opts.oauth2UserCreationAllowed = getBooleanValue("OAUTH2_USER_CREATION")
	opts.oauth2ClientID = getStringValue("OAUTH2_CLIENT_ID", defaultOAuth2ClientID)
	opts.oauth2ClientSecret = getStringValue("OAUTH2_CLIENT_SECRET", defaultOAuth2ClientSecret)
	opts.oauth2RedirectURL = getStringValue("OAUTH2_REDIRECT_URL", defaultOAuth2RedirectURL)
	opts.oauth2Provider = getStringValue("OAUTH2_PROVIDER", defaultOAuth2Provider)
func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
			lines = append(lines, line)
		}
	}
	return lines
}

	opts.httpClientTimeout = getIntValue("HTTP_CLIENT_TIMEOUT", defaultHTTPClientTimeout)
	opts.httpClientMaxBodySize = int64(getIntValue("HTTP_CLIENT_MAX_BODY_SIZE", defaultHTTPClientMaxBodySize) * 1024 * 1024)
func (p *Parser) parseLines(lines []string) (err error) {
	var port string

	for _, line := range lines {
		fields := strings.SplitN(line, "=", 2)
		key := strings.TrimSpace(fields[0])
		value := strings.TrimSpace(fields[1])

		switch key {
		case "DEBUG":
			p.opts.debug = parseBool(value, defaultDebug)
		case "BASE_URL":
			p.opts.baseURL, p.opts.rootURL, p.opts.basePath, err = parseBaseURL(value)
			if err != nil {
				return err
			}
		case "PORT":
			port = value
		case "LISTEN_ADDR":
			p.opts.listenAddr = parseString(value, defaultListenAddr)
		case "DATABASE_URL":
			p.opts.databaseURL = parseString(value, defaultDatabaseURL)
		case "DATABASE_MAX_CONNS":
			p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
		case "DATABASE_MIN_CONNS":
			p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
		case "RUN_MIGRATIONS":
			p.opts.runMigrations = parseBool(value, defaultRunMigrations)
		case "DISABLE_HSTS":
			p.opts.hsts = !parseBool(value, defaultHSTS)
		case "HTTPS":
			p.opts.HTTPS = parseBool(value, defaultHTTPS)
		case "DISABLE_SCHEDULER_SERVICE":
			p.opts.schedulerService = !parseBool(value, defaultSchedulerService)
		case "DISABLE_HTTP_SERVICE":
			p.opts.httpService = !parseBool(value, defaultHTTPService)
		case "CERT_FILE":
			p.opts.certFile = parseString(value, defaultCertFile)
		case "KEY_FILE":
			p.opts.certKeyFile = parseString(value, defaultKeyFile)
		case "CERT_DOMAIN":
			p.opts.certDomain = parseString(value, defaultCertDomain)
		case "CERT_CACHE":
			p.opts.certCache = parseString(value, defaultCertCache)
		case "CLEANUP_FREQUENCY":
			p.opts.cleanupFrequency = parseInt(value, defaultCleanupFrequency)
		case "WORKER_POOL_SIZE":
			p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
		case "POLLING_FREQUENCY":
			p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
		case "BATCH_SIZE":
			p.opts.batchSize = parseInt(value, defaultBatchSize)
		case "ARCHIVE_READ_DAYS":
			p.opts.archiveReadDays = parseInt(value, defaultArchiveReadDays)
		case "PROXY_IMAGES":
			p.opts.proxyImages = parseString(value, defaultProxyImages)
		case "CREATE_ADMIN":
			p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
		case "POCKET_CONSUMER_KEY":
			p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey)
		case "OAUTH2_USER_CREATION":
			p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
		case "OAUTH2_CLIENT_ID":
			p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
		case "OAUTH2_CLIENT_SECRET":
			p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
		case "OAUTH2_REDIRECT_URL":
			p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
		case "OAUTH2_PROVIDER":
			p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
		case "HTTP_CLIENT_TIMEOUT":
			p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
		case "HTTP_CLIENT_MAX_BODY_SIZE":
			p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
		}
	}

	return opts, nil
	if port != "" {
		p.opts.listenAddr = ":" + port
	}
	return nil
}

func parseBaseURL() (string, string, string, error) {
	baseURL := os.Getenv("BASE_URL")
	if baseURL == "" {
		return defaultBaseURL, defaultBaseURL, "", nil
func parseBaseURL(value string) (string, string, string, error) {
	if value == "" {
		return defaultBaseURL, defaultRootURL, "", nil
	}

	if baseURL[len(baseURL)-1:] == "/" {
		baseURL = baseURL[:len(baseURL)-1]
	if value[len(value)-1:] == "/" {
		value = value[:len(value)-1]
	}

	u, err := url.Parse(baseURL)
	url, err := url_parser.Parse(value)
	if err != nil {
		return "", "", "", fmt.Errorf("Invalid BASE_URL: %v", err)
	}

	scheme := strings.ToLower(u.Scheme)
	scheme := strings.ToLower(url.Scheme)
	if scheme != "https" && scheme != "http" {
		return "", "", "", errors.New("Invalid BASE_URL: scheme must be http or https")
	}

	basePath := u.Path
	u.Path = ""
	return baseURL, u.String(), basePath, nil
	basePath := url.Path
	url.Path = ""
	return value, url.String(), basePath, nil
}

func parseListenAddr() string {
	if port := os.Getenv("PORT"); port != "" {
		return ":" + port
func parseBool(value string, fallback bool) bool {
	if value == "" {
		return fallback
	}

	return getStringValue("LISTEN_ADDR", defaultListenAddr)
}

func getBooleanValue(key string) bool {
	value := strings.ToLower(os.Getenv(key))
	value = strings.ToLower(value)
	if value == "1" || value == "yes" || value == "true" || value == "on" {
		return true
	}
	return false
}

func getStringValue(key, fallback string) string {
	value := os.Getenv(key)
	if value == "" {
		return fallback
	}

	return value
	return false
}

func getIntValue(key string, fallback int) int {
	value := os.Getenv(key)
func parseInt(value string, fallback int) int {
	if value == "" {
		return fallback
	}


@@ 123,3 194,10 @@ func getIntValue(key string, fallback int) int {

	return v
}

func parseString(value string, fallback string) string {
	if value == "" {
		return fallback
	}
	return value
}

M config/parser_test.go => config/parser_test.go +13 -31
@@ 5,20 5,12 @@
package config // import "miniflux.app/config"

import (
	"os"
	"testing"
)

func TestGetBooleanValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getBooleanValue("MY_TEST_VARIABLE") {
		t.Errorf(`Unset variables should returns false`)
	}
}

func TestGetBooleanValue(t *testing.T) {
func TestParseBoolValue(t *testing.T) {
	scenarios := map[string]bool{
		"":        false,
		"":        true,
		"1":       true,
		"Yes":     true,
		"yes":     true,


@@ 31,49 23,39 @@ func TestGetBooleanValue(t *testing.T) {
	}

	for input, expected := range scenarios {
		os.Clearenv()
		os.Setenv("MY_TEST_VARIABLE", input)
		result := getBooleanValue("MY_TEST_VARIABLE")
		result := parseBool(input, true)
		if result != expected {
			t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected)
		}
	}
}

func TestGetStringValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getStringValue("MY_TEST_VARIABLE", "defaultValue") != "defaultValue" {
func TestParseStringValueWithUnsetVariable(t *testing.T) {
	if parseString("", "defaultValue") != "defaultValue" {
		t.Errorf(`Unset variables should returns the default value`)
	}
}

func TestGetStringValue(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "test")
	if getStringValue("MY_TEST_VARIABLE", "defaultValue") != "test" {
func TestParseStringValue(t *testing.T) {
	if parseString("test", "defaultValue") != "test" {
		t.Errorf(`Defined variables should returns the specified value`)
	}
}

func TestGetIntValueWithUnsetVariable(t *testing.T) {
	os.Clearenv()
	if getIntValue("MY_TEST_VARIABLE", 42) != 42 {
func TestParseIntValueWithUnsetVariable(t *testing.T) {
	if parseInt("", 42) != 42 {
		t.Errorf(`Unset variables should returns the default value`)
	}
}

func TestGetIntValueWithInvalidInput(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "invalid integer")
	if getIntValue("MY_TEST_VARIABLE", 42) != 42 {
func TestParseIntValueWithInvalidInput(t *testing.T) {
	if parseInt("invalid integer", 42) != 42 {
		t.Errorf(`Invalid integer should returns the default value`)
	}
}

func TestGetIntValue(t *testing.T) {
	os.Clearenv()
	os.Setenv("MY_TEST_VARIABLE", "2018")
	if getIntValue("MY_TEST_VARIABLE", 42) != 2018 {
func TestParseIntValue(t *testing.T) {
	if parseInt("2018", 42) != 2018 {
		t.Errorf(`Defined variables should returns the specified value`)
	}
}

M miniflux.1 => miniflux.1 +17 -2
@@ 5,14 5,29 @@
miniflux \- Minimalist and opinionated feed reader

.SH SYNOPSIS
\fBminiflux\fR [-vi] [-create-admin] [-debug] [-flush-sessions] [-info] [-migrate]
         [-reset-feed-errors] [-reset-password] [-version]
\fBminiflux\fR [-vic] [-create-admin] [-debug] [-flush-sessions] [-info] [-migrate]
         [-reset-feed-errors] [-reset-password] [-version] [-config-file] [-config-dump]

.SH DESCRIPTION
\fBminiflux\fR is a minimalist and opinionated feed reader.

.SH OPTIONS
.PP
.B \-c
.RS 4
Load configuration file\&.
.RE
.PP
.B \-config-file
.RS 4
Load configuration file\&.
.RE
.PP
.B \-config-dump
.RS 4
Print parsed configuration values\&.
.RE
.PP
.B \-create-admin
.RS 4
Create admin user\&.

M template/functions_test.go => template/functions_test.go +56 -8
@@ 134,7 134,13 @@ func TestElapsedTime(t *testing.T) {
func TestProxyFilterWithHttpDefault(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "http-only")
	config.ParseConfig()

	var err error
	parser := config.NewParser()
	config.Opts, err = parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %v`, err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")


@@ 151,7 157,13 @@ func TestProxyFilterWithHttpDefault(t *testing.T) {
func TestProxyFilterWithHttpsDefault(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "http-only")
	config.ParseConfig()

	var err error
	parser := config.NewParser()
	config.Opts, err = parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %v`, err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")


@@ 168,7 180,13 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) {
func TestProxyFilterWithHttpNever(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "none")
	config.ParseConfig()

	var err error
	parser := config.NewParser()
	config.Opts, err = parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %v`, err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")


@@ 185,7 203,13 @@ func TestProxyFilterWithHttpNever(t *testing.T) {
func TestProxyFilterWithHttpsNever(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "none")
	config.ParseConfig()

	var err error
	parser := config.NewParser()
	config.Opts, err = parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %v`, err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")


@@ 202,7 226,13 @@ func TestProxyFilterWithHttpsNever(t *testing.T) {
func TestProxyFilterWithHttpAlways(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "all")
	config.ParseConfig()

	var err error
	parser := config.NewParser()
	config.Opts, err = parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %v`, err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")


@@ 219,7 249,13 @@ func TestProxyFilterWithHttpAlways(t *testing.T) {
func TestProxyFilterWithHttpsAlways(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "all")
	config.ParseConfig()

	var err error
	parser := config.NewParser()
	config.Opts, err = parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %v`, err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")


@@ 236,7 272,13 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) {
func TestProxyFilterWithHttpInvalid(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "invalid")
	config.ParseConfig()

	var err error
	parser := config.NewParser()
	config.Opts, err = parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %v`, err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")


@@ 253,7 295,13 @@ func TestProxyFilterWithHttpInvalid(t *testing.T) {
func TestProxyFilterWithHttpsInvalid(t *testing.T) {
	os.Clearenv()
	os.Setenv("PROXY_IMAGES", "invalid")
	config.ParseConfig()

	var err error
	parser := config.NewParser()
	config.Opts, err = parser.ParseEnvironmentVariables()
	if err != nil {
		t.Fatalf(`Parsing failure: %v`, err)
	}

	r := mux.NewRouter()
	r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")