diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 14c38fda..062b85e9 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -78,7 +78,7 @@ func Parse() { flag.StringVar(&flagExportUserFeeds, "export-user-feeds", "", flagExportUserFeedsHelp) flag.Parse() - cfg := config.NewParser() + cfg := config.NewConfigParser() if flagConfigFile != "" { config.Opts, err = cfg.ParseFile(flagConfigFile) diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 00000000..fde72657 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config // import "miniflux.app/v2/internal/config" + +import "miniflux.app/v2/internal/version" + +// Opts holds parsed configuration options. +var Opts *configOptions + +var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)" diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index 822063e8..00000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,2367 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package config // import "miniflux.app/v2/internal/config" - -import ( - "bytes" - "os" - "reflect" - "slices" - "testing" - "time" -) - -func TestLogFileDefaultValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogFile() != defaultLogFile { - t.Fatalf(`Unexpected log file value, got %q`, opts.LogFile()) - } -} - -func TestLogFileWithCustomFilename(t *testing.T) { - os.Clearenv() - os.Setenv("LOG_FILE", "foobar.log") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - if opts.LogFile() != "foobar.log" { - t.Fatalf(`Unexpected log file value, got %q`, opts.LogFile()) - } -} - -func TestLogFileWithEmptyValue(t *testing.T) { - os.Clearenv() - os.Setenv("LOG_FILE", "") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogFile() != defaultLogFile { - t.Fatalf(`Unexpected log file value, got %q`, opts.LogFile()) - } -} - -func TestLogLevelDefaultValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogLevel() != defaultLogLevel { - t.Fatalf(`Unexpected log level value, got %q`, opts.LogLevel()) - } -} - -func TestLogLevelWithCustomValue(t *testing.T) { - os.Clearenv() - os.Setenv("LOG_LEVEL", "warning") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogLevel() != "warning" { - t.Fatalf(`Unexpected log level value, got %q`, opts.LogLevel()) - } -} - -func TestLogLevelWithInvalidValue(t *testing.T) { - os.Clearenv() - os.Setenv("LOG_LEVEL", "invalid") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogLevel() != defaultLogLevel { - t.Fatalf(`Unexpected log level value, got %q`, opts.LogLevel()) - } -} - -func TestLogDateTimeDefaultValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogDateTime() != defaultLogDateTime { - t.Fatalf(`Unexpected log date time value, got %v`, opts.LogDateTime()) - } -} - -func TestLogDateTimeWithCustomValue(t *testing.T) { - os.Clearenv() - os.Setenv("LOG_DATETIME", "false") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogDateTime() != false { - t.Fatalf(`Unexpected log date time value, got %v`, opts.LogDateTime()) - } -} - -func TestLogDateTimeWithInvalidValue(t *testing.T) { - os.Clearenv() - os.Setenv("LOG_DATETIME", "invalid") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogDateTime() != defaultLogDateTime { - t.Fatalf(`Unexpected log date time value, got %v`, opts.LogDateTime()) - } -} - -func TestLogFormatDefaultValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogFormat() != defaultLogFormat { - t.Fatalf(`Unexpected log format value, got %q`, opts.LogFormat()) - } -} - -func TestLogFormatWithCustomValue(t *testing.T) { - os.Clearenv() - os.Setenv("LOG_FORMAT", "json") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogFormat() != "json" { - t.Fatalf(`Unexpected log format value, got %q`, opts.LogFormat()) - } -} - -func TestLogFormatWithInvalidValue(t *testing.T) { - os.Clearenv() - os.Setenv("LOG_FORMAT", "invalid") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.LogFormat() != defaultLogFormat { - t.Fatalf(`Unexpected log format value, got %q`, opts.LogFormat()) - } -} - -func TestCustomBaseURL(t *testing.T) { - os.Clearenv() - os.Setenv("BASE_URL", "http://example.org") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.BaseURL() != "http://example.org" { - t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL()) - } - - if opts.RootURL() != "http://example.org" { - t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL()) - } - - if opts.BasePath() != "" { - t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath()) - } -} - -func TestCustomBaseURLWithTrailingSlash(t *testing.T) { - os.Clearenv() - os.Setenv("BASE_URL", "http://example.org/folder/") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.BaseURL() != "http://example.org/folder" { - t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL()) - } - - if opts.RootURL() != "http://example.org" { - t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL()) - } - - if opts.BasePath() != "/folder" { - t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath()) - } -} - -func TestCustomBaseURLWithCustomPort(t *testing.T) { - os.Clearenv() - os.Setenv("BASE_URL", "http://example.org:88/folder/") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.BaseURL() != "http://example.org:88/folder" { - t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL()) - } - - if opts.RootURL() != "http://example.org:88" { - t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL()) - } - - if opts.BasePath() != "/folder" { - t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath()) - } -} - -func TestBaseURLWithoutScheme(t *testing.T) { - os.Clearenv() - os.Setenv("BASE_URL", "example.org/folder/") - - _, err := NewParser().ParseEnvironmentVariables() - if err == nil { - t.Fatalf(`Parsing must fail`) - } -} - -func TestBaseURLWithInvalidScheme(t *testing.T) { - os.Clearenv() - os.Setenv("BASE_URL", "ftp://example.org/folder/") - - _, err := NewParser().ParseEnvironmentVariables() - if err == nil { - t.Fatalf(`Parsing must fail`) - } -} - -func TestInvalidBaseURL(t *testing.T) { - os.Clearenv() - os.Setenv("BASE_URL", "http://example|org") - - _, err := NewParser().ParseEnvironmentVariables() - if err == nil { - t.Fatalf(`Parsing must fail`) - } -} - -func TestDefaultBaseURL(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.BaseURL() != defaultBaseURL { - t.Fatalf(`Unexpected base URL, got "%s"`, opts.BaseURL()) - } - - if opts.RootURL() != defaultBaseURL { - t.Fatalf(`Unexpected root URL, got "%s"`, opts.RootURL()) - } - - if opts.BasePath() != "" { - t.Fatalf(`Unexpected base path, got "%s"`, opts.BasePath()) - } -} - -func TestDatabaseURL(t *testing.T) { - os.Clearenv() - os.Setenv("DATABASE_URL", "foobar") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "foobar" - result := opts.DatabaseURL() - - if 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() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultDatabaseURL - result := opts.DatabaseURL() - - if 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() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultDatabaseMaxConns - result := opts.DatabaseMaxConns() - - if result != expected { - t.Fatalf(`Unexpected DATABASE_MAX_CONNS value, got %v instead of %v`, result, expected) - } -} - -func TestDatabaseMaxConns(t *testing.T) { - os.Clearenv() - os.Setenv("DATABASE_MAX_CONNS", "42") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 42 - result := opts.DatabaseMaxConns() - - if result != expected { - t.Fatalf(`Unexpected DATABASE_MAX_CONNS value, got %v instead of %v`, result, expected) - } -} - -func TestDefaultDatabaseMinConnsValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultDatabaseMinConns - result := opts.DatabaseMinConns() - - if result != expected { - t.Fatalf(`Unexpected DATABASE_MIN_CONNS value, got %v instead of %v`, result, expected) - } -} - -func TestDatabaseMinConns(t *testing.T) { - os.Clearenv() - os.Setenv("DATABASE_MIN_CONNS", "42") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 42 - result := opts.DatabaseMinConns() - - if result != expected { - t.Fatalf(`Unexpected DATABASE_MIN_CONNS value, got %v instead of %v`, result, expected) - } -} - -func TestListenAddr(t *testing.T) { - os.Clearenv() - os.Setenv("LISTEN_ADDR", "foobar") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []string{"foobar"} - result := opts.ListenAddr() - - if !reflect.DeepEqual(result, expected) { - t.Fatalf(`Unexpected LISTEN_ADDR value, got %v instead of %v`, result, expected) - } -} - -func TestListenAddrWithPortDefined(t *testing.T) { - os.Clearenv() - os.Setenv("PORT", "3000") - os.Setenv("LISTEN_ADDR", "foobar") // This should be overridden by PORT - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []string{":3000"} - result := opts.ListenAddr() - - if !reflect.DeepEqual(result, expected) { - t.Fatalf(`Unexpected LISTEN_ADDR value when PORT is set, got %v instead of %v`, result, expected) - } -} - -func TestDefaultListenAddrValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []string{defaultListenAddr} - result := opts.ListenAddr() - - if !reflect.DeepEqual(result, expected) { - t.Fatalf(`Unexpected default LISTEN_ADDR value, got %v instead of %v`, result, expected) - } -} - -func TestCertFile(t *testing.T) { - os.Clearenv() - os.Setenv("CERT_FILE", "foobar") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "foobar" - result := opts.CertFile() - - if result != expected { - t.Fatalf(`Unexpected CERT_FILE value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultCertFileValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultCertFile - result := opts.CertFile() - - if result != expected { - t.Fatalf(`Unexpected CERT_FILE value, got %q instead of %q`, result, expected) - } -} - -func TestKeyFile(t *testing.T) { - os.Clearenv() - os.Setenv("KEY_FILE", "foobar") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "foobar" - result := opts.CertKeyFile() - - if result != expected { - t.Fatalf(`Unexpected KEY_FILE value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultKeyFileValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultKeyFile - result := opts.CertKeyFile() - - if result != expected { - t.Fatalf(`Unexpected KEY_FILE value, got %q instead of %q`, result, expected) - } -} - -func TestCertDomain(t *testing.T) { - os.Clearenv() - os.Setenv("CERT_DOMAIN", "example.org") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "example.org" - result := opts.CertDomain() - - if result != expected { - t.Fatalf(`Unexpected CERT_DOMAIN value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultCertDomainValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultCertDomain - result := opts.CertDomain() - - if result != expected { - t.Fatalf(`Unexpected CERT_DOMAIN value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultCleanupFrequencyHoursValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultCleanupFrequency - result := opts.CleanupFrequency() - - if result != expected { - t.Fatalf(`Unexpected CLEANUP_FREQUENCY_HOURS value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "CLEANUP_FREQUENCY_HOURS" - }) - - expectedSerialized := int(defaultCleanupFrequency / time.Hour) - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestCleanupFrequencyHours(t *testing.T) { - os.Clearenv() - os.Setenv("CLEANUP_FREQUENCY_HOURS", "42") - os.Setenv("CLEANUP_FREQUENCY", "19") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 42 * time.Hour - result := opts.CleanupFrequency() - - if result != expected { - t.Fatalf(`Unexpected CLEANUP_FREQUENCY_HOURS value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "CLEANUP_FREQUENCY_HOURS" - }) - - expectedSerialized := 42 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultCleanupArchiveReadDaysValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 60 * 24 * time.Hour - result := opts.CleanupArchiveReadInterval() - - if result != expected { - t.Fatalf(`Unexpected CLEANUP_ARCHIVE_READ_DAYS value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "CLEANUP_ARCHIVE_READ_DAYS" - }) - - expectedSerialized := 60 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestCleanupArchiveReadDays(t *testing.T) { - os.Clearenv() - os.Setenv("CLEANUP_ARCHIVE_READ_DAYS", "7") - os.Setenv("ARCHIVE_READ_DAYS", "19") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 7 * 24 * time.Hour - result := opts.CleanupArchiveReadInterval() - - if result != expected { - t.Fatalf(`Unexpected CLEANUP_ARCHIVE_READ_DAYS value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "CLEANUP_ARCHIVE_READ_DAYS" - }) - - expectedSerialized := 7 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultCleanupRemoveSessionsDaysValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 30 * 24 * time.Hour - result := opts.CleanupRemoveSessionsInterval() - - if result != expected { - t.Fatalf(`Unexpected CLEANUP_REMOVE_SESSIONS_DAYS value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "CLEANUP_REMOVE_SESSIONS_DAYS" - }) - - expectedSerialized := 30 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestCleanupRemoveSessionsDays(t *testing.T) { - os.Clearenv() - os.Setenv("CLEANUP_REMOVE_SESSIONS_DAYS", "7") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 7 * 24 * time.Hour - result := opts.CleanupRemoveSessionsInterval() - - if result != expected { - t.Fatalf(`Unexpected CLEANUP_REMOVE_SESSIONS_DAYS value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "CLEANUP_REMOVE_SESSIONS_DAYS" - }) - - expectedSerialized := 7 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultWorkerPoolSizeValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultWorkerPoolSize - result := opts.WorkerPoolSize() - - if result != expected { - t.Fatalf(`Unexpected WORKER_POOL_SIZE value, got %v instead of %v`, result, expected) - } -} - -func TestWorkerPoolSize(t *testing.T) { - os.Clearenv() - os.Setenv("WORKER_POOL_SIZE", "42") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 42 - result := opts.WorkerPoolSize() - - if result != expected { - t.Fatalf(`Unexpected WORKER_POOL_SIZE value, got %v instead of %v`, result, expected) - } -} - -func TestDefaultPollingFrequencyValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultPollingFrequency - result := opts.PollingFrequency() - - if result != expected { - t.Fatalf(`Unexpected POLLING_FREQUENCY value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "POLLING_FREQUENCY" - }) - - expectedSerialized := int(defaultPollingFrequency / time.Minute) - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestPollingFrequency(t *testing.T) { - os.Clearenv() - os.Setenv("POLLING_FREQUENCY", "42") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 42 * time.Minute - result := opts.PollingFrequency() - - if result != expected { - t.Fatalf(`Unexpected POLLING_FREQUENCY value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "POLLING_FREQUENCY" - }) - - expectedSerialized := 42 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultForceRefreshInterval(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultForceRefreshInterval - result := opts.ForceRefreshInterval() - - if result != expected { - t.Fatalf(`Unexpected FORCE_REFRESH_INTERVAL value, got %v instead of %v`, result, expected) - } -} - -func TestForceRefreshInterval(t *testing.T) { - os.Clearenv() - os.Setenv("FORCE_REFRESH_INTERVAL", "42") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 42 * time.Minute - result := opts.ForceRefreshInterval() - - if result != expected { - t.Fatalf(`Unexpected FORCE_REFRESH_INTERVAL value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "FORCE_REFRESH_INTERVAL" - }) - - expectedSerialized := 42 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultBatchSizeValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultBatchSize - result := opts.BatchSize() - - if result != expected { - t.Fatalf(`Unexpected BATCH_SIZE value, got %v instead of %v`, result, expected) - } -} - -func TestBatchSize(t *testing.T) { - os.Clearenv() - os.Setenv("BATCH_SIZE", "42") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 42 - result := opts.BatchSize() - - if result != expected { - t.Fatalf(`Unexpected BATCH_SIZE value, got %v instead of %v`, result, expected) - } -} - -func TestDefaultPollingSchedulerValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultPollingScheduler - result := opts.PollingScheduler() - - if result != expected { - t.Fatalf(`Unexpected POLLING_SCHEDULER value, got %v instead of %v`, result, expected) - } -} - -func TestPollingScheduler(t *testing.T) { - os.Clearenv() - os.Setenv("POLLING_SCHEDULER", "entry_count_based") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "entry_count_based" - result := opts.PollingScheduler() - - if result != expected { - t.Fatalf(`Unexpected POLLING_SCHEDULER value, got %v instead of %v`, result, expected) - } -} - -func TestDefaultSchedulerEntryFrequencyMaxIntervalValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultSchedulerEntryFrequencyMaxInterval - result := opts.SchedulerEntryFrequencyMaxInterval() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL value, got %v instead of %v`, result, expected) - } -} - -func TestSchedulerEntryFrequencyMaxInterval(t *testing.T) { - os.Clearenv() - os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL", "30") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 30 * time.Minute - result := opts.SchedulerEntryFrequencyMaxInterval() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL" - }) - - expectedSerialized := 30 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultSchedulerEntryFrequencyMinIntervalValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultSchedulerEntryFrequencyMinInterval - result := opts.SchedulerEntryFrequencyMinInterval() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL value, got %v instead of %v`, result, expected) - } -} - -func TestSchedulerEntryFrequencyMinInterval(t *testing.T) { - os.Clearenv() - os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", "30") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 30 * time.Minute - result := opts.SchedulerEntryFrequencyMinInterval() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL" - }) - - expectedSerialized := 30 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultSchedulerEntryFrequencyFactorValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultSchedulerEntryFrequencyFactor - result := opts.SchedulerEntryFrequencyFactor() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ENTRY_FREQUENCY_FACTOR value, got %v instead of %v`, result, expected) - } -} - -func TestSchedulerEntryFrequencyFactor(t *testing.T) { - os.Clearenv() - os.Setenv("SCHEDULER_ENTRY_FREQUENCY_FACTOR", "2") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 2 - result := opts.SchedulerEntryFrequencyFactor() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ENTRY_FREQUENCY_FACTOR value, got %v instead of %v`, result, expected) - } -} - -func TestDefaultSchedulerRoundRobinValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultSchedulerRoundRobinMinInterval - result := opts.SchedulerRoundRobinMinInterval() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MIN_INTERVAL value, got %v instead of %v`, result, expected) - } -} - -func TestSchedulerRoundRobin(t *testing.T) { - os.Clearenv() - os.Setenv("SCHEDULER_ROUND_ROBIN_MIN_INTERVAL", "15") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 15 * time.Minute - result := opts.SchedulerRoundRobinMinInterval() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MIN_INTERVAL value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL" - }) - - expectedSerialized := 15 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultSchedulerRoundRobinMaxIntervalValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultSchedulerRoundRobinMaxInterval - result := opts.SchedulerRoundRobinMaxInterval() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected) - } -} - -func TestSchedulerRoundRobinMaxInterval(t *testing.T) { - os.Clearenv() - os.Setenv("SCHEDULER_ROUND_ROBIN_MAX_INTERVAL", "150") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 150 * time.Minute - result := opts.SchedulerRoundRobinMaxInterval() - - if result != expected { - t.Fatalf(`Unexpected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL value, got %v instead of %v`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL" - }) - - expectedSerialized := 150 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestPollingParsingErrorLimit(t *testing.T) { - os.Clearenv() - os.Setenv("POLLING_PARSING_ERROR_LIMIT", "100") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 100 - result := opts.PollingParsingErrorLimit() - - if result != expected { - t.Fatalf(`Unexpected POLLING_SCHEDULER value, got %v instead of %v`, result, expected) - } -} - -func TestOAuth2UserCreationWhenUnset(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := false - result := opts.IsOAuth2UserCreationAllowed() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_USER_CREATION value, got %v instead of %v`, result, expected) - } -} - -func TestOAuth2UserCreationAdmin(t *testing.T) { - os.Clearenv() - os.Setenv("OAUTH2_USER_CREATION", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.IsOAuth2UserCreationAllowed() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_USER_CREATION value, got %v instead of %v`, result, expected) - } -} - -func TestOAuth2ClientID(t *testing.T) { - os.Clearenv() - os.Setenv("OAUTH2_CLIENT_ID", "foobar") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "foobar" - result := opts.OAuth2ClientID() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_CLIENT_ID value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultOAuth2ClientIDValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultOAuth2ClientID - result := opts.OAuth2ClientID() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_CLIENT_ID value, got %q instead of %q`, result, expected) - } -} - -func TestOAuth2ClientSecret(t *testing.T) { - os.Clearenv() - os.Setenv("OAUTH2_CLIENT_SECRET", "secret") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "secret" - result := opts.OAuth2ClientSecret() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_CLIENT_SECRET value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultOAuth2ClientSecretValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultOAuth2ClientSecret - result := opts.OAuth2ClientSecret() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_CLIENT_SECRET value, got %q instead of %q`, result, expected) - } -} - -func TestOAuth2RedirectURL(t *testing.T) { - os.Clearenv() - os.Setenv("OAUTH2_REDIRECT_URL", "http://example.org") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "http://example.org" - result := opts.OAuth2RedirectURL() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_REDIRECT_URL value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultOAuth2RedirectURLValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultOAuth2RedirectURL - result := opts.OAuth2RedirectURL() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_REDIRECT_URL value, got %q instead of %q`, result, expected) - } -} - -func TestOAuth2OIDCDiscoveryEndpoint(t *testing.T) { - os.Clearenv() - os.Setenv("OAUTH2_OIDC_DISCOVERY_ENDPOINT", "http://example.org") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "http://example.org" - result := opts.OIDCDiscoveryEndpoint() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_OIDC_DISCOVERY_ENDPOINT value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultOIDCDiscoveryEndpointValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultOAuth2OidcDiscoveryEndpoint - result := opts.OIDCDiscoveryEndpoint() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_OIDC_DISCOVERY_ENDPOINT value, got %q instead of %q`, result, expected) - } -} - -func TestOAuth2Provider(t *testing.T) { - os.Clearenv() - os.Setenv("OAUTH2_PROVIDER", "google") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "google" - result := opts.OAuth2Provider() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_PROVIDER value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultOAuth2ProviderValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultOAuth2Provider - result := opts.OAuth2Provider() - - if result != expected { - t.Fatalf(`Unexpected OAUTH2_PROVIDER value, got %q instead of %q`, result, expected) - } -} - -func TestHSTSWhenUnset(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.HasHSTS() - - if result != expected { - t.Fatalf(`Unexpected DISABLE_HSTS value, got %v instead of %v`, result, expected) - } -} - -func TestHSTS(t *testing.T) { - os.Clearenv() - os.Setenv("DISABLE_HSTS", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := false - result := opts.HasHSTS() - - if result != expected { - t.Fatalf(`Unexpected DISABLE_HSTS value, got %v instead of %v`, result, expected) - } -} - -func TestDisableHTTPServiceWhenUnset(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.HasHTTPService() - - if result != expected { - t.Fatalf(`Unexpected DISABLE_HTTP_SERVICE value, got %v instead of %v`, result, expected) - } -} - -func TestDisableHTTPService(t *testing.T) { - os.Clearenv() - os.Setenv("DISABLE_HTTP_SERVICE", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := false - result := opts.HasHTTPService() - - if result != expected { - t.Fatalf(`Unexpected DISABLE_HTTP_SERVICE value, got %v instead of %v`, result, expected) - } -} - -func TestDisableSchedulerServiceWhenUnset(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.HasSchedulerService() - - if result != expected { - t.Fatalf(`Unexpected DISABLE_SCHEDULER_SERVICE value, got %v instead of %v`, result, expected) - } -} - -func TestDisableSchedulerService(t *testing.T) { - os.Clearenv() - os.Setenv("DISABLE_SCHEDULER_SERVICE", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := false - result := opts.HasSchedulerService() - - if result != expected { - t.Fatalf(`Unexpected DISABLE_SCHEDULER_SERVICE value, got %v instead of %v`, result, expected) - } -} - -func TestRunMigrationsWhenUnset(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := false - result := opts.RunMigrations() - - if result != expected { - t.Fatalf(`Unexpected RUN_MIGRATIONS value, got %v instead of %v`, result, expected) - } -} - -func TestRunMigrations(t *testing.T) { - os.Clearenv() - os.Setenv("RUN_MIGRATIONS", "yes") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.RunMigrations() - - if result != expected { - t.Fatalf(`Unexpected RUN_MIGRATIONS value, got %v instead of %v`, result, expected) - } -} - -func TestCreateAdminWhenUnset(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := false - result := opts.CreateAdmin() - - if result != expected { - t.Fatalf(`Unexpected CREATE_ADMIN value, got %v instead of %v`, result, expected) - } -} - -func TestCreateAdmin(t *testing.T) { - os.Clearenv() - os.Setenv("CREATE_ADMIN", "true") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.CreateAdmin() - - if result != expected { - t.Fatalf(`Unexpected CREATE_ADMIN value, got %v instead of %v`, result, expected) - } -} - -func TestMediaProxyMode(t *testing.T) { - os.Clearenv() - os.Setenv("MEDIA_PROXY_MODE", "all") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "all" - result := opts.MediaProxyMode() - - if result != expected { - t.Fatalf(`Unexpected MEDIA_PROXY_MODE value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultMediaProxyModeValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultMediaProxyMode - result := opts.MediaProxyMode() - - if result != expected { - t.Fatalf(`Unexpected MEDIA_PROXY_MODE value, got %q instead of %q`, result, expected) - } -} - -func TestMediaProxyResourceTypes(t *testing.T) { - os.Clearenv() - os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,audio") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []string{"audio", "image"} - - if len(expected) != len(opts.MediaProxyResourceTypes()) { - t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected) - } - - resultMap := make(map[string]bool) - for _, mediaType := range opts.MediaProxyResourceTypes() { - resultMap[mediaType] = true - } - - for _, mediaType := range expected { - if !resultMap[mediaType] { - t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected) - } - } -} - -func TestMediaProxyResourceTypesWithDuplicatedValues(t *testing.T) { - os.Clearenv() - os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image,audio, image") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []string{"audio", "image"} - if len(expected) != len(opts.MediaProxyResourceTypes()) { - t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected) - } - - resultMap := make(map[string]bool) - for _, mediaType := range opts.MediaProxyResourceTypes() { - resultMap[mediaType] = true - } - - for _, mediaType := range expected { - if !resultMap[mediaType] { - t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected) - } - } -} - -func TestDefaultMediaProxyResourceTypes(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []string{"image"} - - if len(expected) != len(opts.MediaProxyResourceTypes()) { - t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected) - } - - resultMap := make(map[string]bool) - for _, mediaType := range opts.MediaProxyResourceTypes() { - resultMap[mediaType] = true - } - - for _, mediaType := range expected { - if !resultMap[mediaType] { - t.Fatalf(`Unexpected MEDIA_PROXY_RESOURCE_TYPES value, got %v instead of %v`, opts.MediaProxyResourceTypes(), expected) - } - } -} - -func TestMediaProxyHTTPClientTimeout(t *testing.T) { - os.Clearenv() - os.Setenv("MEDIA_PROXY_HTTP_CLIENT_TIMEOUT", "24") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 24 * time.Second - result := opts.MediaProxyHTTPClientTimeout() - - if result != expected { - t.Fatalf(`Unexpected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT" - }) - - expectedSerialized := 24 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultMediaProxyHTTPClientTimeoutValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultMediaProxyHTTPClientTimeout - result := opts.MediaProxyHTTPClientTimeout() - - if result != expected { - t.Fatalf(`Unexpected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT" - }) - - expectedSerialized := int(defaultMediaProxyHTTPClientTimeout / time.Second) - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestMediaProxyCustomURL(t *testing.T) { - os.Clearenv() - os.Setenv("MEDIA_PROXY_CUSTOM_URL", "http://example.org/proxy") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - expected := "http://example.org/proxy" - result := opts.MediaCustomProxyURL() - - if result == nil || result.String() != expected { - t.Fatalf(`Unexpected MEDIA_PROXY_CUSTOM_URL value, got %q instead of %q`, result, expected) - } -} - -func TestMediaProxyPrivateKey(t *testing.T) { - os.Clearenv() - os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "foobar") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []byte("foobar") - result := opts.MediaProxyPrivateKey() - - if !bytes.Equal(result, expected) { - t.Fatalf(`Unexpected MEDIA_PROXY_PRIVATE_KEY value, got %q instead of %q`, result, expected) - } -} - -func TestHTTPSOff(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if opts.HTTPS { - t.Fatalf(`Unexpected HTTPS value, got "%v"`, opts.HTTPS) - } -} - -func TestHTTPSOn(t *testing.T) { - os.Clearenv() - os.Setenv("HTTPS", "on") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - if !opts.HTTPS { - t.Fatalf(`Unexpected HTTPS value, got "%v"`, opts.HTTPS) - } -} - -func TestHTTPClientTimeout(t *testing.T) { - os.Clearenv() - os.Setenv("HTTP_CLIENT_TIMEOUT", "42") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 42 * time.Second - result := opts.HTTPClientTimeout() - - if result != expected { - t.Fatalf(`Unexpected HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "HTTP_CLIENT_TIMEOUT" - }) - - expectedSerialized := 42 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultHTTPClientTimeoutValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultHTTPClientTimeout - result := opts.HTTPClientTimeout() - - if result != expected { - t.Fatalf(`Unexpected HTTP_CLIENT_TIMEOUT value, got %d instead of %d`, result, expected) - } -} - -func TestHTTPClientMaxBodySize(t *testing.T) { - os.Clearenv() - os.Setenv("HTTP_CLIENT_MAX_BODY_SIZE", "42") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := int64(42 * 1024 * 1024) - result := opts.HTTPClientMaxBodySize() - - if result != expected { - t.Fatalf(`Unexpected HTTP_CLIENT_MAX_BODY_SIZE value, got %d instead of %d`, result, expected) - } -} - -func TestDefaultHTTPClientMaxBodySizeValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := int64(defaultHTTPClientMaxBodySize * 1024 * 1024) - result := opts.HTTPClientMaxBodySize() - - if result != expected { - t.Fatalf(`Unexpected HTTP_CLIENT_MAX_BODY_SIZE value, got %d instead of %d`, result, expected) - } -} - -func TestHTTPServerTimeout(t *testing.T) { - os.Clearenv() - os.Setenv("HTTP_SERVER_TIMEOUT", "342") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 342 * time.Second - result := opts.HTTPServerTimeout() - - if result != expected { - t.Fatalf(`Unexpected HTTP_SERVER_TIMEOUT value, got %d instead of %d`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "HTTP_SERVER_TIMEOUT" - }) - - expectedSerialized := 342 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultHTTPServerTimeoutValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultHTTPServerTimeout - result := opts.HTTPServerTimeout() - - if result != expected { - t.Fatalf(`Unexpected HTTP_SERVER_TIMEOUT value, got %d instead of %d`, result, expected) - } -} - -func TestParseConfigFile(t *testing.T) { - content := []byte(` - # This is a comment - -LOG_LEVEL = debug - -Invalid text -`) - - tmpfile, err := os.CreateTemp(".", "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.LogLevel() != "debug" { - t.Errorf(`Unexpected log level value, got %q`, opts.LogLevel()) - } - - if err := tmpfile.Close(); err != nil { - t.Fatal(err) - } - - if err := os.Remove(tmpfile.Name()); err != nil { - t.Fatal(err) - } -} - -func TestAuthProxyHeader(t *testing.T) { - os.Clearenv() - os.Setenv("AUTH_PROXY_HEADER", "X-Forwarded-User") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "X-Forwarded-User" - result := opts.AuthProxyHeader() - - if result != expected { - t.Fatalf(`Unexpected AUTH_PROXY_HEADER value, got %q instead of %q`, result, expected) - } -} - -func TestDefaultAuthProxyHeaderValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultAuthProxyHeader - result := opts.AuthProxyHeader() - - if result != expected { - t.Fatalf(`Unexpected AUTH_PROXY_HEADER value, got %q instead of %q`, result, expected) - } -} - -func TestAuthProxyUserCreationWhenUnset(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := false - result := opts.IsAuthProxyUserCreationAllowed() - - if result != expected { - t.Fatalf(`Unexpected AUTH_PROXY_USER_CREATION value, got %v instead of %v`, result, expected) - } -} - -func TestAuthProxyUserCreationAdmin(t *testing.T) { - os.Clearenv() - os.Setenv("AUTH_PROXY_USER_CREATION", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.IsAuthProxyUserCreationAllowed() - - if result != expected { - t.Fatalf(`Unexpected AUTH_PROXY_USER_CREATION value, got %v instead of %v`, result, expected) - } -} - -func TestFetchBilibiliWatchTime(t *testing.T) { - os.Clearenv() - os.Setenv("FETCH_BILIBILI_WATCH_TIME", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.FetchBilibiliWatchTime() - - if result != expected { - t.Fatalf(`Unexpected FETCH_BILIBILI_WATCH_TIME value, got %v instead of %v`, result, expected) - } -} - -func TestFetchNebulaWatchTime(t *testing.T) { - os.Clearenv() - os.Setenv("FETCH_NEBULA_WATCH_TIME", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.FetchNebulaWatchTime() - - if result != expected { - t.Fatalf(`Unexpected FETCH_NEBULA_WATCH_TIME value, got %v instead of %v`, result, expected) - } -} - -func TestFetchOdyseeWatchTime(t *testing.T) { - os.Clearenv() - os.Setenv("FETCH_ODYSEE_WATCH_TIME", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.FetchOdyseeWatchTime() - - if result != expected { - t.Fatalf(`Unexpected FETCH_ODYSEE_WATCH_TIME value, got %v instead of %v`, result, expected) - } -} - -func TestFetchYouTubeWatchTime(t *testing.T) { - os.Clearenv() - os.Setenv("FETCH_YOUTUBE_WATCH_TIME", "1") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := true - result := opts.FetchYouTubeWatchTime() - - if result != expected { - t.Fatalf(`Unexpected FETCH_YOUTUBE_WATCH_TIME value, got %v instead of %v`, result, expected) - } -} - -func TestYouTubeApiKey(t *testing.T) { - os.Clearenv() - os.Setenv("YOUTUBE_API_KEY", "AAAAAAAAAAAAAaaaaaaaaaaaaa0000000000000") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "AAAAAAAAAAAAAaaaaaaaaaaaaa0000000000000" - result := opts.YouTubeApiKey() - - if result != expected { - t.Fatalf(`Unexpected YOUTUBE_API_KEY value, got %v instead of %v`, result, expected) - } -} - -func TestDefaultYouTubeEmbedUrl(t *testing.T) { - os.Clearenv() - - opts, err := NewParser().ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "https://www.youtube-nocookie.com/embed/" - result := opts.YouTubeEmbedUrlOverride() - - if result != expected { - t.Fatalf(`Unexpected default value, got %v instead of %v`, result, expected) - } - - expected = "www.youtube-nocookie.com" - result = opts.YouTubeEmbedDomain() - if result != expected { - t.Fatalf(`Unexpected YouTube embed domain, got %v instead of %v`, result, expected) - } -} - -func TestYouTubeEmbedUrlOverride(t *testing.T) { - os.Clearenv() - os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") - - opts, err := NewParser().ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "https://invidious.custom/embed/" - result := opts.YouTubeEmbedUrlOverride() - - if result != expected { - t.Fatalf(`Unexpected YOUTUBE_EMBED_URL_OVERRIDE value, got %v instead of %v`, result, expected) - } - - expected = "invidious.custom" - result = opts.YouTubeEmbedDomain() - if result != expected { - t.Fatalf(`Unexpected YouTube embed domain, got %v instead of %v`, result, expected) - } -} - -func TestParseConfigDumpOutput(t *testing.T) { - os.Clearenv() - - wantOpts := NewOptions() - wantOpts.adminUsername = "my-username" - - serialized := wantOpts.String() - tmpfile, err := os.CreateTemp(".", "miniflux.*.unit_test.conf") - if err != nil { - t.Fatal(err) - } - - if _, err := tmpfile.WriteString(serialized); err != nil { - t.Fatal(err) - } - - parser := NewParser() - parsedOpts, err := parser.ParseFile(tmpfile.Name()) - if err != nil { - t.Errorf(`Parsing failure: %v`, err) - } - - if parsedOpts.AdminUsername() != wantOpts.AdminUsername() { - t.Fatalf(`Unexpected ADMIN_USERNAME value, got %q instead of %q`, parsedOpts.AdminUsername(), wantOpts.AdminUsername()) - } - - if err := tmpfile.Close(); err != nil { - t.Fatal(err) - } - - if err := os.Remove(tmpfile.Name()); err != nil { - t.Fatal(err) - } -} - -func TestHTTPClientProxies(t *testing.T) { - os.Clearenv() - os.Setenv("HTTP_CLIENT_PROXIES", "http://proxy1.example.com,http://proxy2.example.com") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []string{"http://proxy1.example.com", "http://proxy2.example.com"} - result := opts.HTTPClientProxies() - - if len(expected) != len(result) { - t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected) - } - - for i, proxy := range expected { - if result[i] != proxy { - t.Fatalf(`Unexpected HTTP_CLIENT_PROXIES value at index %d, got %q instead of %q`, i, result[i], proxy) - } - } -} - -func TestDefaultHTTPClientProxiesValue(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := []string{} - result := opts.HTTPClientProxies() - - if len(expected) != len(result) { - t.Fatalf(`Unexpected default HTTP_CLIENT_PROXIES value, got %v instead of %v`, result, expected) - } -} - -func TestHTTPClientProxy(t *testing.T) { - os.Clearenv() - os.Setenv("HTTP_CLIENT_PROXY", "http://proxy.example.com") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := "http://proxy.example.com" - if opts.HTTPClientProxyURL() == nil || opts.HTTPClientProxyURL().String() != expected { - t.Fatalf(`Unexpected HTTP_CLIENT_PROXY value, got %v instead of %v`, opts.HTTPClientProxyURL(), expected) - } -} - -func TestInvalidHTTPClientProxy(t *testing.T) { - os.Clearenv() - os.Setenv("HTTP_CLIENT_PROXY", "sche|me://invalid-proxy-url") - - parser := NewParser() - _, err := parser.ParseEnvironmentVariables() - if err == nil { - t.Fatalf(`Expected error for invalid HTTP_CLIENT_PROXY value, but got none`) - } -} - -func TestDefaultPollingLimitPerHost(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 0 - result := opts.PollingLimitPerHost() - if result != expected { - t.Fatalf(`Unexpected default PollingLimitPerHost value, got %v instead of %v`, result, expected) - } -} - -func TestCustomPollingLimitPerHost(t *testing.T) { - os.Clearenv() - os.Setenv("POLLING_LIMIT_PER_HOST", "10") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 10 - result := opts.PollingLimitPerHost() - if result != expected { - t.Fatalf(`Unexpected custom PollingLimitPerHost value, got %v instead of %v`, result, expected) - } -} - -func TestMetricsRefreshInterval(t *testing.T) { - os.Clearenv() - os.Setenv("METRICS_REFRESH_INTERVAL", "33") - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := 33 * time.Second - result := opts.MetricsRefreshInterval() - - if result != expected { - t.Fatalf(`Unexpected METRICS_REFRESH_INTERVAL value, got %d instead of %d`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "METRICS_REFRESH_INTERVAL" - }) - - expectedSerialized := 33 - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} - -func TestDefaultMetricsRefreshInterval(t *testing.T) { - os.Clearenv() - - parser := NewParser() - opts, err := parser.ParseEnvironmentVariables() - if err != nil { - t.Fatalf(`Parsing failure: %v`, err) - } - - expected := defaultMetricsRefreshInterval - result := opts.MetricsRefreshInterval() - - if result != expected { - t.Fatalf(`Unexpected METRICS_REFRESH_INTERVAL value, got %d instead of %d`, result, expected) - } - - sorted := opts.SortedOptions(false) - i := slices.IndexFunc(sorted, func(opt *option) bool { - return opt.Key == "METRICS_REFRESH_INTERVAL" - }) - - expectedSerialized := int(defaultMetricsRefreshInterval / time.Second) - if got := sorted[i].Value; got != expectedSerialized { - t.Fatalf(`Unexpected value in option output, got %q instead of %q`, got, expectedSerialized) - } -} diff --git a/internal/config/options.go b/internal/config/options.go index 11f5a77d..be8768a6 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -10,811 +10,984 @@ import ( "slices" "strings" "time" - - "miniflux.app/v2/internal/crypto" - "miniflux.app/v2/internal/version" ) +type optionPair struct { + Key string + Value string +} + +type configValueType int + const ( - defaultHTTPS = false - defaultLogFile = "stderr" - defaultLogDateTime = false - defaultLogFormat = "text" - defaultLogLevel = "info" - defaultHSTS = true - defaultHTTPService = true - defaultSchedulerService = true - defaultBaseURL = "http://localhost" - defaultRootURL = "http://localhost" - defaultBasePath = "" - defaultWorkerPoolSize = 16 - defaultPollingFrequency = 60 * time.Minute - defaultForceRefreshInterval = 30 * time.Minute - defaultBatchSize = 100 - defaultPollingScheduler = "round_robin" - defaultSchedulerEntryFrequencyMinInterval = 5 * time.Minute - defaultSchedulerEntryFrequencyMaxInterval = 24 * time.Hour - defaultSchedulerEntryFrequencyFactor = 1 - defaultSchedulerRoundRobinMinInterval = 1 * time.Hour - defaultSchedulerRoundRobinMaxInterval = 24 * time.Hour - defaultPollingParsingErrorLimit = 3 - defaultRunMigrations = false - defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable" - defaultDatabaseMaxConns = 20 - defaultDatabaseMinConns = 1 - defaultDatabaseConnectionLifetime = 5 - defaultListenAddr = "127.0.0.1:8080" - defaultCertFile = "" - defaultKeyFile = "" - defaultCertDomain = "" - defaultCleanupFrequency = 24 * time.Hour - defaultCleanupArchiveReadInterval = 60 * 24 * time.Hour - defaultCleanupArchiveUnreadInterval = 180 * 24 * time.Hour - defaultCleanupArchiveBatchSize = 10000 - defaultCleanupRemoveSessionsInterval = 30 * 24 * time.Hour - defaultMediaProxyHTTPClientTimeout = 120 * time.Second - defaultMediaProxyMode = "http-only" - defaultMediaResourceTypes = "image" - defaultMediaProxyURL = "" - defaultFilterEntryMaxAgeDays = 0 - defaultFetchBilibiliWatchTime = false - defaultFetchNebulaWatchTime = false - defaultFetchOdyseeWatchTime = false - defaultFetchYouTubeWatchTime = false - defaultYouTubeApiKey = "" - defaultYouTubeEmbedUrlOverride = "https://www.youtube-nocookie.com/embed/" - defaultCreateAdmin = false - defaultAdminUsername = "" - defaultAdminPassword = "" - defaultOAuth2UserCreation = false - defaultOAuth2ClientID = "" - defaultOAuth2ClientSecret = "" - defaultOAuth2RedirectURL = "" - defaultOAuth2OidcDiscoveryEndpoint = "" - defaultOauth2OidcProviderName = "OpenID Connect" - defaultOAuth2Provider = "" - defaultDisableLocalAuth = false - defaultHTTPClientTimeout = 20 * time.Second - defaultHTTPClientMaxBodySize = 15 - defaultHTTPClientProxy = "" - defaultHTTPServerTimeout = 300 * time.Second - defaultAuthProxyHeader = "" - defaultAuthProxyUserCreation = false - defaultMaintenanceMode = false - defaultMaintenanceMessage = "Miniflux is currently under maintenance" - defaultMetricsCollector = false - defaultMetricsRefreshInterval = 60 * time.Second - defaultMetricsAllowedNetworks = "127.0.0.1/8" - defaultMetricsUsername = "" - defaultMetricsPassword = "" - defaultWatchdog = true - defaultInvidiousInstance = "yewtu.be" - defaultWebAuthn = false + stringType configValueType = iota + stringListType + boolType + intType + int64Type + urlType + secondType + minuteType + hourType + dayType + secretFileType + bytesType ) -var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)" +type configValue struct { + ParsedStringValue string + ParsedBoolValue bool + ParsedIntValue int + ParsedInt64Value int64 + ParsedDuration time.Duration + ParsedStringList []string + ParsedURLValue *url.URL + ParsedBytesValue []byte -// option contains a key to value map of a single option. It may be used to output debug strings. -type option struct { - Key string - Value any + RawValue string + ValueType configValueType + Secret bool + TargetKey string + + Validator func(string) error } -// Opts holds parsed configuration options. -var Opts *options - -// options contains configuration options. -type options struct { - HTTPS bool - logFile string - logDateTime bool - logFormat string - logLevel string - hsts bool - httpService bool - schedulerService bool - baseURL string - rootURL string - basePath string - databaseURL string - databaseMaxConns int - databaseMinConns int - databaseConnectionLifetime int - runMigrations bool - listenAddr []string - certFile string - certDomain string - certKeyFile string - cleanupFrequencyInterval time.Duration - cleanupArchiveReadInterval time.Duration - cleanupArchiveUnreadInterval time.Duration - cleanupArchiveBatchSize int - cleanupRemoveSessionsInterval time.Duration - forceRefreshInterval time.Duration - batchSize int - schedulerEntryFrequencyMinInterval time.Duration - schedulerEntryFrequencyMaxInterval time.Duration - schedulerEntryFrequencyFactor int - schedulerRoundRobinMinInterval time.Duration - schedulerRoundRobinMaxInterval time.Duration - pollingFrequency time.Duration - pollingLimitPerHost int - pollingParsingErrorLimit int - pollingScheduler string - workerPoolSize int - createAdmin bool - adminUsername string - adminPassword string - mediaProxyHTTPClientTimeout time.Duration - mediaProxyMode string - mediaProxyResourceTypes []string - mediaProxyCustomURL *url.URL - fetchBilibiliWatchTime bool - fetchNebulaWatchTime bool - fetchOdyseeWatchTime bool - fetchYouTubeWatchTime bool - filterEntryMaxAgeDays int - youTubeApiKey string - youTubeEmbedUrlOverride string - youTubeEmbedDomain string - oauth2UserCreationAllowed bool - oauth2ClientID string - oauth2ClientSecret string - oauth2RedirectURL string - oidcDiscoveryEndpoint string - oidcProviderName string - oauth2Provider string - disableLocalAuth bool - httpClientTimeout time.Duration - httpClientMaxBodySize int64 - httpClientProxyURL *url.URL - httpClientProxies []string - httpClientUserAgent string - httpServerTimeout time.Duration - authProxyHeader string - authProxyUserCreation bool - maintenanceMode bool - maintenanceMessage string - metricsCollector bool - metricsRefreshInterval time.Duration - metricsAllowedNetworks []string - metricsUsername string - metricsPassword string - watchdog bool - invidiousInstance string - mediaProxyPrivateKey []byte - webAuthn bool +type configOptions struct { + rootURL string + basePath string + youTubeEmbedDomain string + options map[string]*configValue } -// NewOptions returns Options with default values. -func NewOptions() *options { - return &options{ - HTTPS: defaultHTTPS, - logFile: defaultLogFile, - logDateTime: defaultLogDateTime, - logFormat: defaultLogFormat, - logLevel: defaultLogLevel, - hsts: defaultHSTS, - httpService: defaultHTTPService, - schedulerService: defaultSchedulerService, - baseURL: defaultBaseURL, - rootURL: defaultRootURL, - basePath: defaultBasePath, - databaseURL: defaultDatabaseURL, - databaseMaxConns: defaultDatabaseMaxConns, - databaseMinConns: defaultDatabaseMinConns, - databaseConnectionLifetime: defaultDatabaseConnectionLifetime, - runMigrations: defaultRunMigrations, - listenAddr: []string{defaultListenAddr}, - certFile: defaultCertFile, - certDomain: defaultCertDomain, - certKeyFile: defaultKeyFile, - cleanupFrequencyInterval: defaultCleanupFrequency, - cleanupArchiveReadInterval: defaultCleanupArchiveReadInterval, - cleanupArchiveUnreadInterval: defaultCleanupArchiveUnreadInterval, - cleanupArchiveBatchSize: defaultCleanupArchiveBatchSize, - cleanupRemoveSessionsInterval: defaultCleanupRemoveSessionsInterval, - pollingFrequency: defaultPollingFrequency, - forceRefreshInterval: defaultForceRefreshInterval, - batchSize: defaultBatchSize, - pollingScheduler: defaultPollingScheduler, - schedulerEntryFrequencyMinInterval: defaultSchedulerEntryFrequencyMinInterval, - schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval, - schedulerEntryFrequencyFactor: defaultSchedulerEntryFrequencyFactor, - schedulerRoundRobinMinInterval: defaultSchedulerRoundRobinMinInterval, - schedulerRoundRobinMaxInterval: defaultSchedulerRoundRobinMaxInterval, - pollingParsingErrorLimit: defaultPollingParsingErrorLimit, - workerPoolSize: defaultWorkerPoolSize, - createAdmin: defaultCreateAdmin, - mediaProxyHTTPClientTimeout: defaultMediaProxyHTTPClientTimeout, - mediaProxyMode: defaultMediaProxyMode, - mediaProxyResourceTypes: []string{defaultMediaResourceTypes}, - mediaProxyCustomURL: nil, - filterEntryMaxAgeDays: defaultFilterEntryMaxAgeDays, - fetchBilibiliWatchTime: defaultFetchBilibiliWatchTime, - fetchNebulaWatchTime: defaultFetchNebulaWatchTime, - fetchOdyseeWatchTime: defaultFetchOdyseeWatchTime, - fetchYouTubeWatchTime: defaultFetchYouTubeWatchTime, - youTubeApiKey: defaultYouTubeApiKey, - youTubeEmbedUrlOverride: defaultYouTubeEmbedUrlOverride, - oauth2UserCreationAllowed: defaultOAuth2UserCreation, - oauth2ClientID: defaultOAuth2ClientID, - oauth2ClientSecret: defaultOAuth2ClientSecret, - oauth2RedirectURL: defaultOAuth2RedirectURL, - oidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint, - oidcProviderName: defaultOauth2OidcProviderName, - oauth2Provider: defaultOAuth2Provider, - disableLocalAuth: defaultDisableLocalAuth, - httpClientTimeout: defaultHTTPClientTimeout, - httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024, - httpClientProxyURL: nil, - httpClientProxies: []string{}, - httpClientUserAgent: defaultHTTPClientUserAgent, - httpServerTimeout: defaultHTTPServerTimeout, - authProxyHeader: defaultAuthProxyHeader, - authProxyUserCreation: defaultAuthProxyUserCreation, - maintenanceMode: defaultMaintenanceMode, - maintenanceMessage: defaultMaintenanceMessage, - metricsCollector: defaultMetricsCollector, - metricsRefreshInterval: defaultMetricsRefreshInterval, - metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks}, - metricsUsername: defaultMetricsUsername, - metricsPassword: defaultMetricsPassword, - watchdog: defaultWatchdog, - invidiousInstance: defaultInvidiousInstance, - mediaProxyPrivateKey: crypto.GenerateRandomBytes(16), - webAuthn: defaultWebAuthn, +// NewConfigOptions creates a new instance of ConfigOptions with default values. +func NewConfigOptions() *configOptions { + return &configOptions{ + rootURL: "http://localhost", + basePath: "", + youTubeEmbedDomain: "www.youtube-nocookie.com", + options: map[string]*configValue{ + "ADMIN_PASSWORD": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + Secret: true, + }, + "ADMIN_PASSWORD_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: secretFileType, + TargetKey: "ADMIN_PASSWORD", + }, + "ADMIN_USERNAME": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "ADMIN_USERNAME_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: secretFileType, + TargetKey: "ADMIN_USERNAME", + }, + "AUTH_PROXY_HEADER": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "AUTH_PROXY_USER_CREATION": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "BASE_URL": { + ParsedStringValue: "http://localhost", + RawValue: "http://localhost", + ValueType: stringType, + }, + "BATCH_SIZE": { + ParsedIntValue: 100, + RawValue: "100", + ValueType: intType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "CERT_DOMAIN": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "CERT_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "CLEANUP_ARCHIVE_BATCH_SIZE": { + ParsedIntValue: 10000, + RawValue: "10000", + ValueType: intType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "CLEANUP_ARCHIVE_READ_DAYS": { + ParsedDuration: time.Hour * 24 * 60, + RawValue: "60", + ValueType: dayType, + }, + "CLEANUP_ARCHIVE_UNREAD_DAYS": { + ParsedDuration: time.Hour * 24 * 180, + RawValue: "180", + ValueType: dayType, + }, + "CLEANUP_FREQUENCY_HOURS": { + ParsedDuration: time.Hour * 24, + RawValue: "24", + ValueType: hourType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "CLEANUP_REMOVE_SESSIONS_DAYS": { + ParsedDuration: time.Hour * 24 * 30, + RawValue: "30", + ValueType: dayType, + }, + "CREATE_ADMIN": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "DATABASE_CONNECTION_LIFETIME": { + ParsedDuration: time.Minute * 5, + RawValue: "5", + ValueType: minuteType, + Validator: func(rawValue string) error { + return validateGreaterThan(rawValue, 0) + }, + }, + "DATABASE_MAX_CONNS": { + ParsedIntValue: 20, + RawValue: "20", + ValueType: intType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "DATABASE_MIN_CONNS": { + ParsedIntValue: 1, + RawValue: "1", + ValueType: intType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 0) + }, + }, + "DATABASE_URL": { + ParsedStringValue: "user=postgres password=postgres dbname=miniflux2 sslmode=disable", + RawValue: "user=postgres password=postgres dbname=miniflux2 sslmode=disable", + ValueType: stringType, + Secret: true, + }, + "DATABASE_URL_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: secretFileType, + TargetKey: "DATABASE_URL", + }, + "DISABLE_HSTS": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "DISABLE_HTTP_SERVICE": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "DISABLE_LOCAL_AUTH": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "DISABLE_SCHEDULER_SERVICE": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "FETCH_BILIBILI_WATCH_TIME": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "FETCH_NEBULA_WATCH_TIME": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "FETCH_ODYSEE_WATCH_TIME": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "FETCH_YOUTUBE_WATCH_TIME": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "FILTER_ENTRY_MAX_AGE_DAYS": { + ParsedIntValue: 0, + RawValue: "0", + ValueType: intType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 0) + }, + }, + "FORCE_REFRESH_INTERVAL": { + ParsedDuration: 30 * time.Minute, + RawValue: "30", + ValueType: minuteType, + Validator: func(rawValue string) error { + return validateGreaterThan(rawValue, 0) + }, + }, + "HTTP_CLIENT_MAX_BODY_SIZE": { + ParsedInt64Value: 15, + RawValue: "15", + ValueType: int64Type, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "HTTP_CLIENT_PROXIES": { + ParsedStringList: []string{}, + RawValue: "", + ValueType: stringListType, + Secret: true, + }, + "HTTP_CLIENT_PROXY": { + ParsedURLValue: nil, + RawValue: "", + ValueType: urlType, + Secret: true, + }, + "HTTP_CLIENT_TIMEOUT": { + ParsedDuration: 20 * time.Second, + RawValue: "20", + ValueType: secondType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "HTTP_CLIENT_USER_AGENT": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "HTTP_SERVER_TIMEOUT": { + ParsedDuration: 300 * time.Second, + RawValue: "300", + ValueType: secondType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "HTTPS": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "INVIDIOUS_INSTANCE": { + ParsedStringValue: "yewtu.be", + RawValue: "yewtu.be", + ValueType: stringType, + }, + "KEY_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "LISTEN_ADDR": { + ParsedStringList: []string{"127.0.0.1:8080"}, + RawValue: "127.0.0.1:8080", + ValueType: stringListType, + }, + "LOG_DATE_TIME": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "LOG_FILE": { + ParsedStringValue: "stderr", + RawValue: "stderr", + ValueType: stringType, + }, + "LOG_FORMAT": { + ParsedStringValue: "text", + RawValue: "text", + ValueType: stringType, + Validator: func(rawValue string) error { + return validateChoices(rawValue, []string{"text", "json"}) + }, + }, + "LOG_LEVEL": { + ParsedStringValue: "info", + RawValue: "info", + ValueType: stringType, + Validator: func(rawValue string) error { + return validateChoices(rawValue, []string{"debug", "info", "warning", "error"}) + }, + }, + "MAINTENANCE_MESSAGE": { + ParsedStringValue: "Miniflux is currently under maintenance", + RawValue: "Miniflux is currently under maintenance", + ValueType: stringType, + }, + "MAINTENANCE_MODE": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "MEDIA_PROXY_CUSTOM_URL": { + RawValue: "", + ValueType: urlType, + }, + "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": { + ParsedDuration: 120 * time.Second, + RawValue: "120", + ValueType: secondType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "MEDIA_PROXY_MODE": { + ParsedStringValue: "http-only", + RawValue: "http-only", + ValueType: stringType, + Validator: func(rawValue string) error { + return validateChoices(rawValue, []string{"none", "http-only", "all"}) + }, + }, + "MEDIA_PROXY_PRIVATE_KEY": { + ValueType: bytesType, + Secret: true, + }, + "MEDIA_PROXY_RESOURCE_TYPES": { + ParsedStringList: []string{"image"}, + RawValue: "image", + ValueType: stringListType, + Validator: func(rawValue string) error { + return validateListChoices(strings.Split(rawValue, ","), []string{"image", "video", "audio"}) + }, + }, + "METRICS_ALLOWED_NETWORKS": { + ParsedStringList: []string{"127.0.0.1/8"}, + RawValue: "127.0.0.1/8", + ValueType: stringListType, + }, + "METRICS_COLLECTOR": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "METRICS_PASSWORD": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + Secret: true, + }, + "METRICS_PASSWORD_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: secretFileType, + TargetKey: "METRICS_PASSWORD", + }, + "METRICS_REFRESH_INTERVAL": { + ParsedDuration: 60 * time.Second, + RawValue: "60", + ValueType: secondType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "METRICS_USERNAME": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "METRICS_USERNAME_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: secretFileType, + TargetKey: "METRICS_USERNAME", + }, + "OAUTH2_CLIENT_ID": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + Secret: true, + }, + "OAUTH2_CLIENT_ID_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: secretFileType, + TargetKey: "OAUTH2_CLIENT_ID", + }, + "OAUTH2_CLIENT_SECRET": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + Secret: true, + }, + "OAUTH2_CLIENT_SECRET_FILE": { + ParsedStringValue: "", + RawValue: "", + ValueType: secretFileType, + TargetKey: "OAUTH2_CLIENT_SECRET", + }, + "OAUTH2_OIDC_DISCOVERY_ENDPOINT": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "OAUTH2_OIDC_PROVIDER_NAME": { + ParsedStringValue: "OpenID Connect", + RawValue: "OpenID Connect", + ValueType: stringType, + }, + "OAUTH2_PROVIDER": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + Validator: func(rawValue string) error { + return validateChoices(rawValue, []string{"oidc", "google"}) + }, + }, + "OAUTH2_REDIRECT_URL": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + }, + "OAUTH2_USER_CREATION": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "POLLING_FREQUENCY": { + ParsedDuration: 60 * time.Minute, + RawValue: "60", + ValueType: minuteType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "POLLING_LIMIT_PER_HOST": { + ParsedIntValue: 0, + RawValue: "0", + ValueType: intType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 0) + }, + }, + "POLLING_PARSING_ERROR_LIMIT": { + ParsedIntValue: 3, + RawValue: "3", + ValueType: intType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 0) + }, + }, + "POLLING_SCHEDULER": { + ParsedStringValue: "round_robin", + RawValue: "round_robin", + ValueType: stringType, + Validator: func(rawValue string) error { + return validateChoices(rawValue, []string{"round_robin", "entry_frequency"}) + }, + }, + "PORT": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + Validator: func(rawValue string) error { + return validateRange(rawValue, 1, 65535) + }, + }, + "RUN_MIGRATIONS": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "SCHEDULER_ENTRY_FREQUENCY_FACTOR": { + ParsedIntValue: 1, + RawValue: "1", + ValueType: intType, + }, + "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": { + ParsedDuration: 24 * time.Hour, + RawValue: "1440", + ValueType: minuteType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": { + ParsedDuration: 5 * time.Minute, + RawValue: "5", + ValueType: minuteType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": { + ParsedDuration: 1440 * time.Minute, + RawValue: "1440", + ValueType: minuteType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": { + ParsedDuration: 60 * time.Minute, + RawValue: "60", + ValueType: minuteType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "WATCHDOG": { + ParsedBoolValue: true, + RawValue: "1", + ValueType: boolType, + }, + "WEBAUTHN": { + ParsedBoolValue: false, + RawValue: "0", + ValueType: boolType, + }, + "WORKER_POOL_SIZE": { + ParsedIntValue: 16, + RawValue: "16", + ValueType: intType, + Validator: func(rawValue string) error { + return validateGreaterOrEqualThan(rawValue, 1) + }, + }, + "YOUTUBE_API_KEY": { + ParsedStringValue: "", + RawValue: "", + ValueType: stringType, + Secret: true, + }, + "YOUTUBE_EMBED_URL_OVERRIDE": { + ParsedStringValue: "https://www.youtube-nocookie.com/embed/", + RawValue: "https://www.youtube-nocookie.com/embed/", + ValueType: stringType, + }, + }, } } -func (o *options) LogFile() string { - return o.logFile +func (c *configOptions) AdminPassword() string { + return c.options["ADMIN_PASSWORD"].ParsedStringValue } -// LogDateTime returns true if the date/time should be displayed in log messages. -func (o *options) LogDateTime() bool { - return o.logDateTime +func (c *configOptions) AdminUsername() string { + return c.options["ADMIN_USERNAME"].ParsedStringValue } -// LogFormat returns the log format. -func (o *options) LogFormat() string { - return o.logFormat +func (c *configOptions) AuthProxyHeader() string { + return c.options["AUTH_PROXY_HEADER"].ParsedStringValue } -// LogLevel returns the log level. -func (o *options) LogLevel() string { - return o.logLevel +func (c *configOptions) AuthProxyUserCreation() bool { + return c.options["AUTH_PROXY_USER_CREATION"].ParsedBoolValue } -// SetLogLevel sets the log level. -func (o *options) SetLogLevel(level string) { - o.logLevel = level +func (c *configOptions) BasePath() string { + return c.basePath } -// HasMaintenanceMode returns true if maintenance mode is enabled. -func (o *options) HasMaintenanceMode() bool { - return o.maintenanceMode +func (c *configOptions) BaseURL() string { + return c.options["BASE_URL"].ParsedStringValue } -// MaintenanceMessage returns maintenance message. -func (o *options) MaintenanceMessage() string { - return o.maintenanceMessage +func (c *configOptions) RootURL() string { + return c.rootURL } -// BaseURL returns the application base URL with path. -func (o *options) BaseURL() string { - return o.baseURL +func (c *configOptions) BatchSize() int { + return c.options["BATCH_SIZE"].ParsedIntValue } -// RootURL returns the base URL without path. -func (o *options) RootURL() string { - return o.rootURL +func (c *configOptions) CertDomain() string { + return c.options["CERT_DOMAIN"].ParsedStringValue } -// BasePath returns the application base path according to the base URL. -func (o *options) BasePath() string { - return o.basePath +func (c *configOptions) CertFile() string { + return c.options["CERT_FILE"].ParsedStringValue } -// IsDefaultDatabaseURL returns true if the default database URL is used. -func (o *options) IsDefaultDatabaseURL() bool { - return o.databaseURL == defaultDatabaseURL +func (c *configOptions) CleanupArchiveBatchSize() int { + return c.options["CLEANUP_ARCHIVE_BATCH_SIZE"].ParsedIntValue } -// DatabaseURL returns the database URL. -func (o *options) DatabaseURL() string { - return o.databaseURL +func (c *configOptions) CleanupArchiveReadInterval() time.Duration { + return c.options["CLEANUP_ARCHIVE_READ_DAYS"].ParsedDuration } -// DatabaseMaxConns returns the maximum number of database connections. -func (o *options) DatabaseMaxConns() int { - return o.databaseMaxConns +func (c *configOptions) CleanupArchiveUnreadInterval() time.Duration { + return c.options["CLEANUP_ARCHIVE_UNREAD_DAYS"].ParsedDuration } -// DatabaseMinConns returns the minimum number of database connections. -func (o *options) DatabaseMinConns() int { - return o.databaseMinConns +func (c *configOptions) CleanupFrequency() time.Duration { + return c.options["CLEANUP_FREQUENCY_HOURS"].ParsedDuration } -// DatabaseConnectionLifetime returns the maximum amount of time a connection may be reused. -func (o *options) DatabaseConnectionLifetime() time.Duration { - return time.Duration(o.databaseConnectionLifetime) * time.Minute +func (c *configOptions) CleanupRemoveSessionsInterval() time.Duration { + return c.options["CLEANUP_REMOVE_SESSIONS_DAYS"].ParsedDuration } -// ListenAddr returns the listen address for the HTTP server. -func (o *options) ListenAddr() []string { - return o.listenAddr +func (c *configOptions) CreateAdmin() bool { + return c.options["CREATE_ADMIN"].ParsedBoolValue } -// CertFile returns the SSL certificate filename if any. -func (o *options) CertFile() string { - return o.certFile +func (c *configOptions) DatabaseConnectionLifetime() time.Duration { + return c.options["DATABASE_CONNECTION_LIFETIME"].ParsedDuration } -// CertKeyFile returns the private key filename for custom SSL certificate. -func (o *options) CertKeyFile() string { - return o.certKeyFile +func (c *configOptions) DatabaseMaxConns() int { + return c.options["DATABASE_MAX_CONNS"].ParsedIntValue } -// CertDomain returns the domain to use for Let's Encrypt certificate. -func (o *options) CertDomain() string { - return o.certDomain +func (c *configOptions) DatabaseMinConns() int { + return c.options["DATABASE_MIN_CONNS"].ParsedIntValue } -// CleanupFrequencyHours returns the interval for cleanup jobs. -func (o *options) CleanupFrequency() time.Duration { - return o.cleanupFrequencyInterval +func (c *configOptions) DatabaseURL() string { + return c.options["DATABASE_URL"].ParsedStringValue } -// CleanupArchiveReadDays returns the interval after which marking read items as removed. -func (o *options) CleanupArchiveReadInterval() time.Duration { - return o.cleanupArchiveReadInterval +func (c *configOptions) DisableHSTS() bool { + return c.options["DISABLE_HSTS"].ParsedBoolValue } -// CleanupArchiveUnreadDays returns the interval after which marking unread items as removed. -func (o *options) CleanupArchiveUnreadInterval() time.Duration { - return o.cleanupArchiveUnreadInterval +func (c *configOptions) DisableHTTPService() bool { + return c.options["DISABLE_HTTP_SERVICE"].ParsedBoolValue } -// CleanupArchiveBatchSize returns the number of entries to archive for each interval. -func (o *options) CleanupArchiveBatchSize() int { - return o.cleanupArchiveBatchSize +func (c *configOptions) DisableLocalAuth() bool { + return c.options["DISABLE_LOCAL_AUTH"].ParsedBoolValue } -// CleanupRemoveSessionsDays returns the interval after which to remove sessions. -func (o *options) CleanupRemoveSessionsInterval() time.Duration { - return o.cleanupRemoveSessionsInterval +func (c *configOptions) DisableSchedulerService() bool { + return c.options["DISABLE_SCHEDULER_SERVICE"].ParsedBoolValue } -// WorkerPoolSize returns the number of background worker. -func (o *options) WorkerPoolSize() int { - return o.workerPoolSize +func (c *configOptions) FetchBilibiliWatchTime() bool { + return c.options["FETCH_BILIBILI_WATCH_TIME"].ParsedBoolValue } -// ForceRefreshInterval returns the force refresh interval -func (o *options) ForceRefreshInterval() time.Duration { - return o.forceRefreshInterval +func (c *configOptions) FetchNebulaWatchTime() bool { + return c.options["FETCH_NEBULA_WATCH_TIME"].ParsedBoolValue } -// BatchSize returns the number of feeds to send for background processing. -func (o *options) BatchSize() int { - return o.batchSize +func (c *configOptions) FetchOdyseeWatchTime() bool { + return c.options["FETCH_ODYSEE_WATCH_TIME"].ParsedBoolValue } -// PollingFrequency returns the interval to refresh feeds in the background. -func (o *options) PollingFrequency() time.Duration { - return o.pollingFrequency +func (c *configOptions) FetchYouTubeWatchTime() bool { + return c.options["FETCH_YOUTUBE_WATCH_TIME"].ParsedBoolValue } -// PollingLimitPerHost returns the limit of concurrent requests per host. -// Set to zero to disable. -func (o *options) PollingLimitPerHost() int { - return o.pollingLimitPerHost +func (c *configOptions) FilterEntryMaxAgeDays() int { + return c.options["FILTER_ENTRY_MAX_AGE_DAYS"].ParsedIntValue } -// PollingParsingErrorLimit returns the limit of errors when to stop polling. -func (o *options) PollingParsingErrorLimit() int { - return o.pollingParsingErrorLimit +func (c *configOptions) ForceRefreshInterval() time.Duration { + return c.options["FORCE_REFRESH_INTERVAL"].ParsedDuration } -// PollingScheduler returns the scheduler used for polling feeds. -func (o *options) PollingScheduler() string { - return o.pollingScheduler +func (c *configOptions) HasHTTPClientProxiesConfigured() bool { + return len(c.options["HTTP_CLIENT_PROXIES"].ParsedStringList) > 0 } -// SchedulerEntryFrequencyMaxInterval returns the maximum interval for the entry frequency scheduler. -func (o *options) SchedulerEntryFrequencyMaxInterval() time.Duration { - return o.schedulerEntryFrequencyMaxInterval +func (c *configOptions) HasHTTPService() bool { + return !c.options["DISABLE_HTTP_SERVICE"].ParsedBoolValue } -// SchedulerEntryFrequencyMinInterval returns the minimum interval for the entry frequency scheduler. -func (o *options) SchedulerEntryFrequencyMinInterval() time.Duration { - return o.schedulerEntryFrequencyMinInterval +func (c *configOptions) HasHSTS() bool { + return !c.options["DISABLE_HSTS"].ParsedBoolValue } -// SchedulerEntryFrequencyFactor returns the factor for the entry frequency scheduler. -func (o *options) SchedulerEntryFrequencyFactor() int { - return o.schedulerEntryFrequencyFactor +func (c *configOptions) HasHTTPClientProxyURLConfigured() bool { + return c.options["HTTP_CLIENT_PROXY"].ParsedURLValue != nil } -func (o *options) SchedulerRoundRobinMinInterval() time.Duration { - return o.schedulerRoundRobinMinInterval +func (c *configOptions) HasMaintenanceMode() bool { + return c.options["MAINTENANCE_MODE"].ParsedBoolValue } -func (o *options) SchedulerRoundRobinMaxInterval() time.Duration { - return o.schedulerRoundRobinMaxInterval +func (c *configOptions) HasMetricsCollector() bool { + return c.options["METRICS_COLLECTOR"].ParsedBoolValue } -// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users. -func (o *options) IsOAuth2UserCreationAllowed() bool { - return o.oauth2UserCreationAllowed +func (c *configOptions) HasSchedulerService() bool { + return !c.options["DISABLE_SCHEDULER_SERVICE"].ParsedBoolValue } -// OAuth2ClientID returns the OAuth2 Client ID. -func (o *options) OAuth2ClientID() string { - return o.oauth2ClientID +func (c *configOptions) HasWatchdog() bool { + return c.options["WATCHDOG"].ParsedBoolValue } -// OAuth2ClientSecret returns the OAuth2 client secret. -func (o *options) OAuth2ClientSecret() string { - return o.oauth2ClientSecret +func (c *configOptions) HTTPClientMaxBodySize() int64 { + return c.options["HTTP_CLIENT_MAX_BODY_SIZE"].ParsedInt64Value * 1024 * 1024 } -// OAuth2RedirectURL returns the OAuth2 redirect URL. -func (o *options) OAuth2RedirectURL() string { - return o.oauth2RedirectURL +func (c *configOptions) HTTPClientProxies() []string { + return c.options["HTTP_CLIENT_PROXIES"].ParsedStringList } -// OIDCDiscoveryEndpoint returns the OAuth2 OIDC discovery endpoint. -func (o *options) OIDCDiscoveryEndpoint() string { - return o.oidcDiscoveryEndpoint +func (c *configOptions) HTTPClientProxyURL() *url.URL { + return c.options["HTTP_CLIENT_PROXY"].ParsedURLValue } -// OIDCProviderName returns the OAuth2 OIDC provider's display name -func (o *options) OIDCProviderName() string { - return o.oidcProviderName +func (c *configOptions) HTTPClientTimeout() time.Duration { + return c.options["HTTP_CLIENT_TIMEOUT"].ParsedDuration } -// OAuth2Provider returns the name of the OAuth2 provider configured. -func (o *options) OAuth2Provider() string { - return o.oauth2Provider -} - -// DisableLocalAUth returns true if the local user database should not be used to authenticate users -func (o *options) DisableLocalAuth() bool { - return o.disableLocalAuth -} - -// HasHSTS returns true if HTTP Strict Transport Security is enabled. -func (o *options) HasHSTS() bool { - return o.hsts -} - -// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty. -func (o *options) RunMigrations() bool { - return o.runMigrations -} - -// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty. -func (o *options) CreateAdmin() bool { - return o.createAdmin -} - -// AdminUsername returns the admin username if defined. -func (o *options) AdminUsername() string { - return o.adminUsername -} - -// AdminPassword returns the admin password if defined. -func (o *options) AdminPassword() string { - return o.adminPassword -} - -// FetchYouTubeWatchTime returns true if the YouTube video duration -// should be fetched and used as a reading time. -func (o *options) FetchYouTubeWatchTime() bool { - return o.fetchYouTubeWatchTime -} - -// YouTubeApiKey returns the YouTube API key if defined. -func (o *options) YouTubeApiKey() string { - return o.youTubeApiKey -} - -// YouTubeEmbedUrlOverride returns the YouTube embed URL override if defined. -func (o *options) YouTubeEmbedUrlOverride() string { - return o.youTubeEmbedUrlOverride -} - -// YouTubeEmbedDomain returns the domain used for YouTube embeds. -func (o *options) YouTubeEmbedDomain() string { - if o.youTubeEmbedDomain != "" { - return o.youTubeEmbedDomain +func (c *configOptions) HTTPClientUserAgent() string { + if c.options["HTTP_CLIENT_USER_AGENT"].ParsedStringValue != "" { + return c.options["HTTP_CLIENT_USER_AGENT"].ParsedStringValue } - return "www.youtube-nocookie.com" + return defaultHTTPClientUserAgent } -// FetchNebulaWatchTime returns true if the Nebula video duration -// should be fetched and used as a reading time. -func (o *options) FetchNebulaWatchTime() bool { - return o.fetchNebulaWatchTime +func (c *configOptions) HTTPServerTimeout() time.Duration { + return c.options["HTTP_SERVER_TIMEOUT"].ParsedDuration } -// FetchOdyseeWatchTime returns true if the Odysee video duration -// should be fetched and used as a reading time. -func (o *options) FetchOdyseeWatchTime() bool { - return o.fetchOdyseeWatchTime +func (c *configOptions) HTTPS() bool { + return c.options["HTTPS"].ParsedBoolValue } -// FetchBilibiliWatchTime returns true if the Bilibili video duration -// should be fetched and used as a reading time. -func (o *options) FetchBilibiliWatchTime() bool { - return o.fetchBilibiliWatchTime +func (c *configOptions) InvidiousInstance() string { + return c.options["INVIDIOUS_INSTANCE"].ParsedStringValue } -// MediaProxyMode returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy. -func (o *options) MediaProxyMode() string { - return o.mediaProxyMode +func (c *configOptions) IsAuthProxyUserCreationAllowed() bool { + return c.options["AUTH_PROXY_USER_CREATION"].ParsedBoolValue } -// MediaProxyResourceTypes returns a slice of resource types to proxy. -func (o *options) MediaProxyResourceTypes() []string { - return o.mediaProxyResourceTypes +func (c *configOptions) IsDefaultDatabaseURL() bool { + return c.options["DATABASE_URL"].RawValue == "user=postgres password=postgres dbname=miniflux2 sslmode=disable" } -// MediaCustomProxyURL returns the custom proxy URL for medias. -func (o *options) MediaCustomProxyURL() *url.URL { - return o.mediaProxyCustomURL +func (c *configOptions) IsOAuth2UserCreationAllowed() bool { + return c.options["OAUTH2_USER_CREATION"].ParsedBoolValue } -// MediaProxyHTTPClientTimeout returns the time limit before the proxy HTTP client cancel the request. -func (o *options) MediaProxyHTTPClientTimeout() time.Duration { - return o.mediaProxyHTTPClientTimeout +func (c *configOptions) CertKeyFile() string { + return c.options["KEY_FILE"].ParsedStringValue } -// MediaProxyPrivateKey returns the private key used by the media proxy. -func (o *options) MediaProxyPrivateKey() []byte { - return o.mediaProxyPrivateKey +func (c *configOptions) ListenAddr() []string { + return c.options["LISTEN_ADDR"].ParsedStringList } -// HasHTTPService returns true if the HTTP service is enabled. -func (o *options) HasHTTPService() bool { - return o.httpService +func (c *configOptions) LogFile() string { + return c.options["LOG_FILE"].ParsedStringValue } -// HasSchedulerService returns true if the scheduler service is enabled. -func (o *options) HasSchedulerService() bool { - return o.schedulerService +func (c *configOptions) LogDateTime() bool { + return c.options["LOG_DATE_TIME"].ParsedBoolValue } -// HTTPClientTimeout returns the time limit in seconds before the HTTP client cancel the request. -func (o *options) HTTPClientTimeout() time.Duration { - return o.httpClientTimeout +func (c *configOptions) LogFormat() string { + return c.options["LOG_FORMAT"].ParsedStringValue } -// HTTPClientMaxBodySize returns the number of bytes allowed for the HTTP client to transfer. -func (o *options) HTTPClientMaxBodySize() int64 { - return o.httpClientMaxBodySize +func (c *configOptions) LogLevel() string { + return c.options["LOG_LEVEL"].ParsedStringValue } -// HTTPClientProxyURL returns the client HTTP proxy URL if configured. -func (o *options) HTTPClientProxyURL() *url.URL { - return o.httpClientProxyURL +func (c *configOptions) MaintenanceMessage() string { + return c.options["MAINTENANCE_MESSAGE"].ParsedStringValue } -// HasHTTPClientProxyURLConfigured returns true if the client HTTP proxy URL if configured. -func (o *options) HasHTTPClientProxyURLConfigured() bool { - return o.httpClientProxyURL != nil +func (c *configOptions) MaintenanceMode() bool { + return c.options["MAINTENANCE_MODE"].ParsedBoolValue } -// HTTPClientProxies returns the list of proxies. -func (o *options) HTTPClientProxies() []string { - return o.httpClientProxies +func (c *configOptions) MediaCustomProxyURL() *url.URL { + return c.options["MEDIA_PROXY_CUSTOM_URL"].ParsedURLValue } -// HTTPClientProxiesString returns true if the list of rotating proxies are configured. -func (o *options) HasHTTPClientProxiesConfigured() bool { - return len(o.httpClientProxies) > 0 +func (c *configOptions) MediaProxyHTTPClientTimeout() time.Duration { + return c.options["MEDIA_PROXY_HTTP_CLIENT_TIMEOUT"].ParsedDuration } -// HTTPServerTimeout returns the time limit before the HTTP server cancel the request. -func (o *options) HTTPServerTimeout() time.Duration { - return o.httpServerTimeout +func (c *configOptions) MediaProxyMode() string { + return c.options["MEDIA_PROXY_MODE"].ParsedStringValue } -// AuthProxyHeader returns an HTTP header name that contains username for -// authentication using auth proxy. -func (o *options) AuthProxyHeader() string { - return o.authProxyHeader +func (c *configOptions) MediaProxyPrivateKey() []byte { + return c.options["MEDIA_PROXY_PRIVATE_KEY"].ParsedBytesValue } -// IsAuthProxyUserCreationAllowed returns true if user creation is allowed for -// users authenticated using auth proxy. -func (o *options) IsAuthProxyUserCreationAllowed() bool { - return o.authProxyUserCreation +func (c *configOptions) MediaProxyResourceTypes() []string { + return c.options["MEDIA_PROXY_RESOURCE_TYPES"].ParsedStringList } -// HasMetricsCollector returns true if metrics collection is enabled. -func (o *options) HasMetricsCollector() bool { - return o.metricsCollector +func (c *configOptions) MetricsAllowedNetworks() []string { + return c.options["METRICS_ALLOWED_NETWORKS"].ParsedStringList } -// MetricsRefreshInterval returns the refresh interval. -func (o *options) MetricsRefreshInterval() time.Duration { - return o.metricsRefreshInterval +func (c *configOptions) MetricsCollector() bool { + return c.options["METRICS_COLLECTOR"].ParsedBoolValue } -// MetricsAllowedNetworks returns the list of networks allowed to connect to the metrics endpoint. -func (o *options) MetricsAllowedNetworks() []string { - return o.metricsAllowedNetworks +func (c *configOptions) MetricsPassword() string { + return c.options["METRICS_PASSWORD"].ParsedStringValue } -func (o *options) MetricsUsername() string { - return o.metricsUsername +func (c *configOptions) MetricsRefreshInterval() time.Duration { + return c.options["METRICS_REFRESH_INTERVAL"].ParsedDuration } -func (o *options) MetricsPassword() string { - return o.metricsPassword +func (c *configOptions) MetricsUsername() string { + return c.options["METRICS_USERNAME"].ParsedStringValue } -// HTTPClientUserAgent returns the global User-Agent header for miniflux. -func (o *options) HTTPClientUserAgent() string { - return o.httpClientUserAgent +func (c *configOptions) OAuth2ClientID() string { + return c.options["OAUTH2_CLIENT_ID"].ParsedStringValue } -// HasWatchdog returns true if the systemd watchdog is enabled. -func (o *options) HasWatchdog() bool { - return o.watchdog +func (c *configOptions) OAuth2ClientSecret() string { + return c.options["OAUTH2_CLIENT_SECRET"].ParsedStringValue } -// InvidiousInstance returns the invidious instance used by miniflux -func (o *options) InvidiousInstance() string { - return o.invidiousInstance +func (c *configOptions) OAuth2OIDCDiscoveryEndpoint() string { + return c.options["OAUTH2_OIDC_DISCOVERY_ENDPOINT"].ParsedStringValue } -// WebAuthn returns true if WebAuthn logins are supported -func (o *options) WebAuthn() bool { - return o.webAuthn +func (c *configOptions) OAuth2OIDCProviderName() string { + return c.options["OAUTH2_OIDC_PROVIDER_NAME"].ParsedStringValue } -// FilterEntryMaxAgeDays returns the number of days after which entries should be retained. -func (o *options) FilterEntryMaxAgeDays() int { - return o.filterEntryMaxAgeDays +func (c *configOptions) OAuth2Provider() string { + return c.options["OAUTH2_PROVIDER"].ParsedStringValue } -// SortedOptions returns options as a list of key value pairs, sorted by keys. -func (o *options) SortedOptions(redactSecret bool) []*option { - var clientProxyURLRedacted string - if o.httpClientProxyURL != nil { - if redactSecret { - clientProxyURLRedacted = o.httpClientProxyURL.Redacted() - } else { - clientProxyURLRedacted = o.httpClientProxyURL.String() - } +func (c *configOptions) OAuth2RedirectURL() string { + return c.options["OAUTH2_REDIRECT_URL"].ParsedStringValue +} + +func (c *configOptions) OAuth2UserCreation() bool { + return c.options["OAUTH2_USER_CREATION"].ParsedBoolValue +} + +func (c *configOptions) PollingFrequency() time.Duration { + return c.options["POLLING_FREQUENCY"].ParsedDuration +} + +func (c *configOptions) PollingLimitPerHost() int { + return c.options["POLLING_LIMIT_PER_HOST"].ParsedIntValue +} + +func (c *configOptions) PollingParsingErrorLimit() int { + return c.options["POLLING_PARSING_ERROR_LIMIT"].ParsedIntValue +} + +func (c *configOptions) PollingScheduler() string { + return c.options["POLLING_SCHEDULER"].ParsedStringValue +} + +func (c *configOptions) Port() string { + return c.options["PORT"].ParsedStringValue +} + +func (c *configOptions) RunMigrations() bool { + return c.options["RUN_MIGRATIONS"].ParsedBoolValue +} + +func (c *configOptions) SetLogLevel(level string) { + c.options["LOG_LEVEL"].ParsedStringValue = level + c.options["LOG_LEVEL"].RawValue = level +} + +func (c *configOptions) SetHTTPSValue(value bool) { + c.options["HTTPS"].ParsedBoolValue = value + if value { + c.options["HTTPS"].RawValue = "1" + } else { + c.options["HTTPS"].RawValue = "0" } +} - var clientProxyURLsRedacted string - if len(o.httpClientProxies) > 0 { - if redactSecret { - var proxyURLs []string - for range o.httpClientProxies { - proxyURLs = append(proxyURLs, "") - } - clientProxyURLsRedacted = strings.Join(proxyURLs, ",") - } else { - clientProxyURLsRedacted = strings.Join(o.httpClientProxies, ",") - } - } +func (c *configOptions) SchedulerEntryFrequencyFactor() int { + return c.options["SCHEDULER_ENTRY_FREQUENCY_FACTOR"].ParsedIntValue +} - var mediaProxyPrivateKeyValue string - if len(o.mediaProxyPrivateKey) > 0 { - mediaProxyPrivateKeyValue = "" - } +func (c *configOptions) SchedulerEntryFrequencyMaxInterval() time.Duration { + return c.options["SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL"].ParsedDuration +} - var keyValues = map[string]any{ - "ADMIN_PASSWORD": redactSecretValue(o.adminPassword, redactSecret), - "ADMIN_USERNAME": o.adminUsername, - "AUTH_PROXY_HEADER": o.authProxyHeader, - "AUTH_PROXY_USER_CREATION": o.authProxyUserCreation, - "BASE_PATH": o.basePath, - "BASE_URL": o.baseURL, - "BATCH_SIZE": o.batchSize, - "CERT_DOMAIN": o.certDomain, - "CERT_FILE": o.certFile, - "CLEANUP_FREQUENCY_HOURS": int(o.cleanupFrequencyInterval.Hours()), - "CLEANUP_ARCHIVE_BATCH_SIZE": o.cleanupArchiveBatchSize, - "CLEANUP_ARCHIVE_READ_DAYS": int(o.cleanupArchiveReadInterval.Hours() / 24), - "CLEANUP_ARCHIVE_UNREAD_DAYS": int(o.cleanupArchiveUnreadInterval.Hours() / 24), - "CLEANUP_REMOVE_SESSIONS_DAYS": int(o.cleanupRemoveSessionsInterval.Hours() / 24), - "CREATE_ADMIN": o.createAdmin, - "DATABASE_CONNECTION_LIFETIME": o.databaseConnectionLifetime, - "DATABASE_MAX_CONNS": o.databaseMaxConns, - "DATABASE_MIN_CONNS": o.databaseMinConns, - "DATABASE_URL": redactSecretValue(o.databaseURL, redactSecret), - "DISABLE_HSTS": !o.hsts, - "DISABLE_HTTP_SERVICE": !o.httpService, - "DISABLE_SCHEDULER_SERVICE": !o.schedulerService, - "FILTER_ENTRY_MAX_AGE_DAYS": o.filterEntryMaxAgeDays, - "FETCH_YOUTUBE_WATCH_TIME": o.fetchYouTubeWatchTime, - "FETCH_NEBULA_WATCH_TIME": o.fetchNebulaWatchTime, - "FETCH_ODYSEE_WATCH_TIME": o.fetchOdyseeWatchTime, - "FETCH_BILIBILI_WATCH_TIME": o.fetchBilibiliWatchTime, - "HTTPS": o.HTTPS, - "HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize, - "HTTP_CLIENT_PROXIES": clientProxyURLsRedacted, - "HTTP_CLIENT_PROXY": clientProxyURLRedacted, - "HTTP_CLIENT_TIMEOUT": int(o.httpClientTimeout.Seconds()), - "HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent, - "HTTP_SERVER_TIMEOUT": int(o.httpServerTimeout.Seconds()), - "HTTP_SERVICE": o.httpService, - "INVIDIOUS_INSTANCE": o.invidiousInstance, - "KEY_FILE": o.certKeyFile, - "LISTEN_ADDR": strings.Join(o.listenAddr, ","), - "LOG_FILE": o.logFile, - "LOG_DATE_TIME": o.logDateTime, - "LOG_FORMAT": o.logFormat, - "LOG_LEVEL": o.logLevel, - "MAINTENANCE_MESSAGE": o.maintenanceMessage, - "MAINTENANCE_MODE": o.maintenanceMode, - "METRICS_ALLOWED_NETWORKS": strings.Join(o.metricsAllowedNetworks, ","), - "METRICS_COLLECTOR": o.metricsCollector, - "METRICS_PASSWORD": redactSecretValue(o.metricsPassword, redactSecret), - "METRICS_REFRESH_INTERVAL": int(o.metricsRefreshInterval.Seconds()), - "METRICS_USERNAME": o.metricsUsername, - "OAUTH2_CLIENT_ID": o.oauth2ClientID, - "OAUTH2_CLIENT_SECRET": redactSecretValue(o.oauth2ClientSecret, redactSecret), - "OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oidcDiscoveryEndpoint, - "OAUTH2_OIDC_PROVIDER_NAME": o.oidcProviderName, - "OAUTH2_PROVIDER": o.oauth2Provider, - "OAUTH2_REDIRECT_URL": o.oauth2RedirectURL, - "OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed, - "DISABLE_LOCAL_AUTH": o.disableLocalAuth, - "FORCE_REFRESH_INTERVAL": int(o.forceRefreshInterval.Minutes()), - "POLLING_FREQUENCY": int(o.pollingFrequency.Minutes()), - "POLLING_LIMIT_PER_HOST": o.pollingLimitPerHost, - "POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit, - "POLLING_SCHEDULER": o.pollingScheduler, - "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": int(o.mediaProxyHTTPClientTimeout.Seconds()), - "MEDIA_PROXY_RESOURCE_TYPES": o.mediaProxyResourceTypes, - "MEDIA_PROXY_MODE": o.mediaProxyMode, - "MEDIA_PROXY_PRIVATE_KEY": mediaProxyPrivateKeyValue, - "MEDIA_PROXY_CUSTOM_URL": o.mediaProxyCustomURL, - "ROOT_URL": o.rootURL, - "RUN_MIGRATIONS": o.runMigrations, - "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": int(o.schedulerEntryFrequencyMaxInterval.Minutes()), - "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": int(o.schedulerEntryFrequencyMinInterval.Minutes()), - "SCHEDULER_ENTRY_FREQUENCY_FACTOR": o.schedulerEntryFrequencyFactor, - "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": int(o.schedulerRoundRobinMinInterval.Minutes()), - "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": int(o.schedulerRoundRobinMaxInterval.Minutes()), - "SCHEDULER_SERVICE": o.schedulerService, - "WATCHDOG": o.watchdog, - "WORKER_POOL_SIZE": o.workerPoolSize, - "YOUTUBE_API_KEY": redactSecretValue(o.youTubeApiKey, redactSecret), - "YOUTUBE_EMBED_URL_OVERRIDE": o.youTubeEmbedUrlOverride, - "WEBAUTHN": o.webAuthn, - } +func (c *configOptions) SchedulerEntryFrequencyMinInterval() time.Duration { + return c.options["SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL"].ParsedDuration +} - sortedKeys := slices.Sorted(maps.Keys(keyValues)) - var sortedOptions = make([]*option, 0, len(sortedKeys)) +func (c *configOptions) SchedulerRoundRobinMaxInterval() time.Duration { + return c.options["SCHEDULER_ROUND_ROBIN_MAX_INTERVAL"].ParsedDuration +} + +func (c *configOptions) SchedulerRoundRobinMinInterval() time.Duration { + return c.options["SCHEDULER_ROUND_ROBIN_MIN_INTERVAL"].ParsedDuration +} + +func (c *configOptions) Watchdog() bool { + return c.options["WATCHDOG"].ParsedBoolValue +} + +func (c *configOptions) WebAuthn() bool { + return c.options["WEBAUTHN"].ParsedBoolValue +} + +func (c *configOptions) WorkerPoolSize() int { + return c.options["WORKER_POOL_SIZE"].ParsedIntValue +} + +func (c *configOptions) YouTubeAPIKey() string { + return c.options["YOUTUBE_API_KEY"].ParsedStringValue +} + +func (c *configOptions) YouTubeEmbedUrlOverride() string { + return c.options["YOUTUBE_EMBED_URL_OVERRIDE"].ParsedStringValue +} + +func (c *configOptions) YouTubeEmbedDomain() string { + return c.youTubeEmbedDomain +} + +func (c *configOptions) ConfigMap(redactSecret bool) []*optionPair { + sortedKeys := slices.Sorted(maps.Keys(c.options)) + sortedOptions := make([]*optionPair, 0, len(sortedKeys)) for _, key := range sortedKeys { - sortedOptions = append(sortedOptions, &option{Key: key, Value: keyValues[key]}) + value := c.options[key] + displayValue := value.RawValue + if redactSecret && value.Secret && displayValue != "" { + displayValue = "" + } + sortedOptions = append(sortedOptions, &optionPair{Key: key, Value: displayValue}) } return sortedOptions } -func (o *options) String() string { +func (c *configOptions) String() string { var builder strings.Builder - for _, option := range o.SortedOptions(false) { + for _, option := range c.ConfigMap(false) { fmt.Fprintf(&builder, "%s=%v\n", option.Key, option.Value) } return builder.String() } - -func redactSecretValue(value string, redactSecret bool) string { - if redactSecret && value != "" { - return "" - } - return value -} diff --git a/internal/config/options_parsing_test.go b/internal/config/options_parsing_test.go new file mode 100644 index 00000000..cf965644 --- /dev/null +++ b/internal/config/options_parsing_test.go @@ -0,0 +1,1688 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config // import "miniflux.app/v2/internal/config" + +import "testing" + +func TestBaseURLOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.BaseURL() != "http://localhost" { + t.Fatalf("Expected BASE_URL to be 'http://localhost' by default") + } + + if configParser.options.RootURL() != "http://localhost" { + t.Fatalf("Expected ROOT_URL to be 'http://localhost' by default") + } + + if configParser.options.BasePath() != "" { + t.Fatalf("Expected BASE_PATH to be empty by default") + } + + if err := configParser.parseLines([]string{"BASE_URL=https://example.com/app"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.BaseURL() != "https://example.com/app" { + t.Fatalf("Expected BASE_URL to be 'https://example.com/app', got '%s'", configParser.options.BaseURL()) + } + + if configParser.options.RootURL() != "https://example.com" { + t.Fatalf("Expected ROOT_URL to be 'https://example.com', got '%s'", configParser.options.RootURL()) + } + + if configParser.options.BasePath() != "/app" { + t.Fatalf("Expected BASE_PATH to be '/app', got '%s'", configParser.options.BasePath()) + } + + if err := configParser.parseLines([]string{"BASE_URL=https://example.com/app/"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.BaseURL() != "https://example.com/app" { + t.Fatalf("Expected BASE_URL to be 'https://example.com/app', got '%s'", configParser.options.BaseURL()) + } + + if configParser.options.RootURL() != "https://example.com" { + t.Fatalf("Expected ROOT_URL to be 'https://example.com', got '%s'", configParser.options.RootURL()) + } + + if configParser.options.BasePath() != "/app" { + t.Fatalf("Expected BASE_PATH to be '/app', got '%s'", configParser.options.BasePath()) + } + + if err := configParser.parseLines([]string{"BASE_URL=example.com/app/"}); err == nil { + t.Fatal("Expected an error due to missing scheme in BASE_URL") + } +} + +func TestWatchdogOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if !configParser.options.Watchdog() { + t.Fatal("Expected WATCHDOG to be enabled by default") + } + + if !configParser.options.HasSchedulerService() { + t.Fatal("Expected HAS_SCHEDULER_SERVICE to be enabled by default") + } + + if err := configParser.parseLines([]string{"WATCHDOG=1"}); err != nil { + t.Fatal("Unexpected error:", err) + } + + if !configParser.options.Watchdog() { + t.Fatal("Expected WATCHDOG to be enabled") + } + + if !configParser.options.HasSchedulerService() { + t.Fatal("Expected HAS_SCHEDULER_SERVICE to be enabled") + } + + if err := configParser.parseLines([]string{"WATCHDOG=0"}); err != nil { + t.Fatal("Unexpected error:", err) + } + + if configParser.options.Watchdog() { + t.Fatal("Expected WATCHDOG to be disabled") + } + + if configParser.options.HasWatchdog() { + t.Fatal("Expected HAS_WATCHDOG to be disabled") + } +} + +func TestWebAuthnOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.WebAuthn() { + t.Fatalf("Expected WEBAUTHN to be disabled by default") + } + + if err := configParser.parseLines([]string{"WEBAUTHN=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.WebAuthn() { + t.Fatalf("Expected WEBAUTHN to be enabled") + } +} + +func TestWorkerPoolSizeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.WorkerPoolSize() != 16 { + t.Fatalf("Expected WORKER_POOL_SIZE to be 16 by default") + } + + if err := configParser.parseLines([]string{"WORKER_POOL_SIZE=8"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.WorkerPoolSize() != 8 { + t.Fatalf("Expected WORKER_POOL_SIZE to be 8") + } +} + +func TestYouTubeAPIKeyOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.YouTubeAPIKey() != "" { + t.Fatalf("Expected YOUTUBE_API_KEY to be empty by default") + } + + if err := configParser.parseLines([]string{"YOUTUBE_API_KEY=somekey"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.YouTubeAPIKey() != "somekey" { + t.Fatalf("Expected YOUTUBE_API_KEY to be 'somekey'") + } +} + +func TestAdminPasswordOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.AdminPassword() != "" { + t.Fatalf("Expected ADMIN_PASSWORD to be empty by default") + } + + if err := configParser.parseLines([]string{"ADMIN_PASSWORD=secret123"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.AdminPassword() != "secret123" { + t.Fatalf("Expected ADMIN_PASSWORD to be 'secret123'") + } +} + +func TestAdminUsernameOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.AdminUsername() != "" { + t.Fatalf("Expected ADMIN_USERNAME to be empty by default") + } + + if err := configParser.parseLines([]string{"ADMIN_USERNAME=admin"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.AdminUsername() != "admin" { + t.Fatalf("Expected ADMIN_USERNAME to be 'admin'") + } +} + +func TestAuthProxyHeaderOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.AuthProxyHeader() != "" { + t.Fatalf("Expected AUTH_PROXY_HEADER to be empty by default") + } + + if err := configParser.parseLines([]string{"AUTH_PROXY_HEADER=X-Forwarded-User"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.AuthProxyHeader() != "X-Forwarded-User" { + t.Fatalf("Expected AUTH_PROXY_HEADER to be 'X-Forwarded-User'") + } +} + +func TestAuthProxyUserCreationOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.AuthProxyUserCreation() { + t.Fatal("Expected AUTH_PROXY_USER_CREATION to be disabled by default") + } + + if configParser.options.IsAuthProxyUserCreationAllowed() { + t.Fatal("Expected HAS_AUTH_PROXY_USER_CREATION to be disabled by default") + } + + if err := configParser.parseLines([]string{"AUTH_PROXY_USER_CREATION=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.AuthProxyUserCreation() { + t.Fatal("Expected AUTH_PROXY_USER_CREATION to be enabled") + } + + if !configParser.options.IsAuthProxyUserCreationAllowed() { + t.Fatal("Expected HAS_AUTH_PROXY_USER_CREATION to be enabled") + } + + if err := configParser.parseLines([]string{"AUTH_PROXY_USER_CREATION=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.AuthProxyUserCreation() { + t.Fatal("Expected AUTH_PROXY_USER_CREATION to be disabled") + } + + if configParser.options.IsAuthProxyUserCreationAllowed() { + t.Fatal("Expected HAS_AUTH_PROXY_USER_CREATION to be disabled") + } +} + +func TestBatchSizeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.BatchSize() != 100 { + t.Fatalf("Expected BATCH_SIZE to be 100 by default") + } + + if err := configParser.parseLines([]string{"BATCH_SIZE=50"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.BatchSize() != 50 { + t.Fatalf("Expected BATCH_SIZE to be 50") + } +} + +func TestCertDomainOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CertDomain() != "" { + t.Fatalf("Expected CERT_DOMAIN to be empty by default") + } + + if err := configParser.parseLines([]string{"CERT_DOMAIN=example.com"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CertDomain() != "example.com" { + t.Fatalf("Expected CERT_DOMAIN to be 'example.com'") + } +} + +func TestCertFileOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CertFile() != "" { + t.Fatalf("Expected CERT_FILE to be empty by default") + } + + if err := configParser.parseLines([]string{"CERT_FILE=/path/to/cert.pem"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CertFile() != "/path/to/cert.pem" { + t.Fatalf("Expected CERT_FILE to be '/path/to/cert.pem'") + } +} + +func TestCleanupArchiveBatchSizeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CleanupArchiveBatchSize() != 10000 { + t.Fatalf("Expected CLEANUP_ARCHIVE_BATCH_SIZE to be 10000 by default") + } + + if err := configParser.parseLines([]string{"CLEANUP_ARCHIVE_BATCH_SIZE=5000"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CleanupArchiveBatchSize() != 5000 { + t.Fatalf("Expected CLEANUP_ARCHIVE_BATCH_SIZE to be 5000") + } +} + +func TestCreateAdminOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CreateAdmin() { + t.Fatalf("Expected CREATE_ADMIN to be disabled by default") + } + + if err := configParser.parseLines([]string{"CREATE_ADMIN=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.CreateAdmin() { + t.Fatalf("Expected CREATE_ADMIN to be enabled") + } + + if err := configParser.parseLines([]string{"CREATE_ADMIN=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CreateAdmin() { + t.Fatalf("Expected CREATE_ADMIN to be disabled") + } +} + +func TestDatabaseMaxConnsOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.DatabaseMaxConns() != 20 { + t.Fatalf("Expected DATABASE_MAX_CONNS to be 20 by default") + } + + if err := configParser.parseLines([]string{"DATABASE_MAX_CONNS=10"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.DatabaseMaxConns() != 10 { + t.Fatalf("Expected DATABASE_MAX_CONNS to be 10") + } +} + +func TestDatabaseMinConnsOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.DatabaseMinConns() != 1 { + t.Fatalf("Expected DATABASE_MIN_CONNS to be 1 by default") + } + + if err := configParser.parseLines([]string{"DATABASE_MIN_CONNS=2"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.DatabaseMinConns() != 2 { + t.Fatalf("Expected DATABASE_MIN_CONNS to be 2") + } +} + +func TestDatabaseURLOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.DatabaseURL() != "user=postgres password=postgres dbname=miniflux2 sslmode=disable" { + t.Fatal("Expected DATABASE_URL to have default value") + } + + if !configParser.options.IsDefaultDatabaseURL() { + t.Fatal("Expected DATABASE_URL to be the default value") + } + + if err := configParser.parseLines([]string{"DATABASE_URL=postgres://user:pass@localhost/db"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.DatabaseURL() != "postgres://user:pass@localhost/db" { + t.Fatal("Expected DATABASE_URL to be 'postgres://user:pass@localhost/db'") + } + + if configParser.options.IsDefaultDatabaseURL() { + t.Fatal("Expected DATABASE_URL to not be the default value") + } +} + +func TestDisableHSTSOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.DisableHSTS() { + t.Fatal("Expected DISABLE_HSTS to be disabled by default") + } + + if !configParser.options.HasHSTS() { + t.Fatal("Expected HAS_HSTS to be enabled by default") + } + + if err := configParser.parseLines([]string{"DISABLE_HSTS=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.DisableHSTS() { + t.Fatal("Expected DISABLE_HSTS to be enabled") + } + + if configParser.options.HasHSTS() { + t.Fatal("Expected HAS_HSTS to be disabled") + } + + if err := configParser.parseLines([]string{"DISABLE_HSTS=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.DisableHSTS() { + t.Fatal("Expected DISABLE_HSTS to be disabled") + } + + if !configParser.options.HasHSTS() { + t.Fatal("Expected HAS_HSTS to be enabled") + } +} + +func TestDisableHTTPServiceOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.DisableHTTPService() { + t.Fatal("Expected DISABLE_HTTP_SERVICE to be disabled by default") + } + + if !configParser.options.HasHTTPService() { + t.Fatal("Expected HAS_HTTP_SERVICE to be enabled by default") + } + + if err := configParser.parseLines([]string{"DISABLE_HTTP_SERVICE=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.DisableHTTPService() { + t.Fatal("Expected DISABLE_HTTP_SERVICE to be enabled") + } + + if configParser.options.HasHTTPService() { + t.Fatal("Expected HAS_HTTP_SERVICE to be disabled") + } + + if err := configParser.parseLines([]string{"DISABLE_HTTP_SERVICE=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.DisableHTTPService() { + t.Fatal("Expected DISABLE_HTTP_SERVICE to be disabled") + } + + if !configParser.options.HasHTTPService() { + t.Fatal("Expected HAS_HTTP_SERVICE to be disabled") + } +} + +func TestDisableLocalAuthOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.DisableLocalAuth() { + t.Fatalf("Expected DISABLE_LOCAL_AUTH to be disabled by default") + } + + if err := configParser.parseLines([]string{"DISABLE_LOCAL_AUTH=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.DisableLocalAuth() { + t.Fatalf("Expected DISABLE_LOCAL_AUTH to be enabled") + } + + if err := configParser.parseLines([]string{"DISABLE_LOCAL_AUTH=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.DisableLocalAuth() { + t.Fatalf("Expected DISABLE_LOCAL_AUTH to be disabled") + } +} + +func TestDisableSchedulerServiceOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.DisableSchedulerService() { + t.Fatal("Expected DISABLE_SCHEDULER_SERVICE to be disabled by default") + } + + if !configParser.options.HasSchedulerService() { + t.Fatal("Expected HAS_SCHEDULER_SERVICE to be enabled by default") + } + + if err := configParser.parseLines([]string{"DISABLE_SCHEDULER_SERVICE=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.DisableSchedulerService() { + t.Fatal("Expected DISABLE_SCHEDULER_SERVICE to be enabled") + } + + if configParser.options.HasSchedulerService() { + t.Fatal("Expected HAS_SCHEDULER_SERVICE to be disabled") + } + + if err := configParser.parseLines([]string{"DISABLE_SCHEDULER_SERVICE=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.DisableSchedulerService() { + t.Fatal("Expected DISABLE_SCHEDULER_SERVICE to be disabled") + } + + if !configParser.options.HasSchedulerService() { + t.Fatal("Expected HAS_SCHEDULER_SERVICE to be enabled") + } +} + +func TestFetchBilibiliWatchTimeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.FetchBilibiliWatchTime() { + t.Fatalf("Expected FETCH_BILIBILI_WATCH_TIME to be disabled by default") + } + + if err := configParser.parseLines([]string{"FETCH_BILIBILI_WATCH_TIME=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.FetchBilibiliWatchTime() { + t.Fatalf("Expected FETCH_BILIBILI_WATCH_TIME to be enabled") + } + + if err := configParser.parseLines([]string{"FETCH_BILIBILI_WATCH_TIME=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.FetchBilibiliWatchTime() { + t.Fatalf("Expected FETCH_BILIBILI_WATCH_TIME to be disabled") + } +} + +func TestFetchNebulaWatchTimeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.FetchNebulaWatchTime() { + t.Fatalf("Expected FETCH_NEBULA_WATCH_TIME to be disabled by default") + } + + if err := configParser.parseLines([]string{"FETCH_NEBULA_WATCH_TIME=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.FetchNebulaWatchTime() { + t.Fatalf("Expected FETCH_NEBULA_WATCH_TIME to be enabled") + } + + if err := configParser.parseLines([]string{"FETCH_NEBULA_WATCH_TIME=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.FetchNebulaWatchTime() { + t.Fatalf("Expected FETCH_NEBULA_WATCH_TIME to be disabled") + } +} + +func TestFetchOdyseeWatchTimeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.FetchOdyseeWatchTime() { + t.Fatalf("Expected FETCH_ODYSEE_WATCH_TIME to be disabled by default") + } + + if err := configParser.parseLines([]string{"FETCH_ODYSEE_WATCH_TIME=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.FetchOdyseeWatchTime() { + t.Fatalf("Expected FETCH_ODYSEE_WATCH_TIME to be enabled") + } + + if err := configParser.parseLines([]string{"FETCH_ODYSEE_WATCH_TIME=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.FetchOdyseeWatchTime() { + t.Fatalf("Expected FETCH_ODYSEE_WATCH_TIME to be disabled") + } +} + +func TestFetchYouTubeWatchTimeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.FetchYouTubeWatchTime() { + t.Fatalf("Expected FETCH_YOUTUBE_WATCH_TIME to be disabled by default") + } + + if err := configParser.parseLines([]string{"FETCH_YOUTUBE_WATCH_TIME=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.FetchYouTubeWatchTime() { + t.Fatalf("Expected FETCH_YOUTUBE_WATCH_TIME to be enabled") + } + + if err := configParser.parseLines([]string{"FETCH_YOUTUBE_WATCH_TIME=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.FetchYouTubeWatchTime() { + t.Fatalf("Expected FETCH_YOUTUBE_WATCH_TIME to be disabled") + } +} + +func TestHTTPClientMaxBodySizeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.HTTPClientMaxBodySize() != 15*1024*1024 { + t.Fatalf("Expected HTTP_CLIENT_MAX_BODY_SIZE to be 15 by default, got %d", configParser.options.HTTPClientMaxBodySize()) + } + + if err := configParser.parseLines([]string{"HTTP_CLIENT_MAX_BODY_SIZE=25"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + expectedValue := 25 * 1024 * 1024 + currentValue := configParser.options.HTTPClientMaxBodySize() + if currentValue != int64(expectedValue) { + t.Fatalf("Expected HTTP_CLIENT_MAX_BODY_SIZE to be %d, got %d", expectedValue, currentValue) + } +} + +func TestHTTPClientUserAgentOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.HTTPClientUserAgent() != defaultHTTPClientUserAgent { + t.Fatalf("Expected HTTP_CLIENT_USER_AGENT to have default value") + } + + if err := configParser.parseLines([]string{"HTTP_CLIENT_USER_AGENT=Custom User Agent"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.HTTPClientUserAgent() != "Custom User Agent" { + t.Fatalf("Expected HTTP_CLIENT_USER_AGENT to be 'Custom User Agent'") + } +} + +func TestHTTPSOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.HTTPS() { + t.Fatalf("Expected HTTPS to be disabled by default") + } + + if err := configParser.parseLines([]string{"HTTPS=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.HTTPS() { + t.Fatalf("Expected HTTPS to be enabled") + } + + if err := configParser.parseLines([]string{"HTTPS=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.HTTPS() { + t.Fatalf("Expected HTTPS to be disabled") + } +} + +func TestInvidiousInstanceOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.InvidiousInstance() != "yewtu.be" { + t.Fatalf("Expected INVIDIOUS_INSTANCE to be 'yewtu.be' by default") + } + + if err := configParser.parseLines([]string{"INVIDIOUS_INSTANCE=invidious.example.com"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.InvidiousInstance() != "invidious.example.com" { + t.Fatalf("Expected INVIDIOUS_INSTANCE to be 'invidious.example.com'") + } +} + +func TestCertKeyFileOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CertKeyFile() != "" { + t.Fatalf("Expected KEY_FILE to be empty by default") + } + + if err := configParser.parseLines([]string{"KEY_FILE=/path/to/key.pem"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CertKeyFile() != "/path/to/key.pem" { + t.Fatalf("Expected KEY_FILE to be '/path/to/key.pem'") + } +} + +func TestLogDateTimeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.LogDateTime() { + t.Fatalf("Expected LOG_DATE_TIME to be disabled by default") + } + + if err := configParser.parseLines([]string{"LOG_DATE_TIME=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.LogDateTime() { + t.Fatalf("Expected LOG_DATE_TIME to be enabled") + } + + if err := configParser.parseLines([]string{"LOG_DATE_TIME=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.LogDateTime() { + t.Fatalf("Expected LOG_DATE_TIME to be disabled") + } +} + +func TestLogFileOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.LogFile() != "stderr" { + t.Fatalf("Expected LOG_FILE to be 'stderr' by default") + } + + if err := configParser.parseLines([]string{"LOG_FILE=/var/log/miniflux.log"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.LogFile() != "/var/log/miniflux.log" { + t.Fatalf("Expected LOG_FILE to be '/var/log/miniflux.log'") + } +} + +func TestLogFormatOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.LogFormat() != "text" { + t.Fatalf("Expected LOG_FORMAT to be 'text' by default") + } + + if err := configParser.parseLines([]string{"LOG_FORMAT=json"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.LogFormat() != "json" { + t.Fatalf("Expected LOG_FORMAT to be 'json'") + } +} + +func TestLogLevelOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.LogLevel() != "info" { + t.Fatalf("Expected LOG_LEVEL to be 'info' by default") + } + + if err := configParser.parseLines([]string{"LOG_LEVEL=debug"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.LogLevel() != "debug" { + t.Fatalf("Expected LOG_LEVEL to be 'debug'") + } +} + +func TestMaintenanceMessageOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MaintenanceMessage() != "Miniflux is currently under maintenance" { + t.Fatalf("Expected MAINTENANCE_MESSAGE to have default value") + } + + if err := configParser.parseLines([]string{"MAINTENANCE_MESSAGE=System upgrade in progress"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.MaintenanceMessage() != "System upgrade in progress" { + t.Fatalf("Expected MAINTENANCE_MESSAGE to be 'System upgrade in progress'") + } +} + +func TestMaintenanceModeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MaintenanceMode() { + t.Fatal("Expected MAINTENANCE_MODE to be disabled by default") + } + + if configParser.options.HasMaintenanceMode() { + t.Fatal("Expected HAS_MAINTENANCE_MODE to be disabled by default") + } + + if err := configParser.parseLines([]string{"MAINTENANCE_MODE=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.MaintenanceMode() { + t.Fatal("Expected MAINTENANCE_MODE to be enabled") + } + + if !configParser.options.HasMaintenanceMode() { + t.Fatal("Expected HAS_MAINTENANCE_MODE to be enabled") + } + + if err := configParser.parseLines([]string{"MAINTENANCE_MODE=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.MaintenanceMode() { + t.Fatal("Expected MAINTENANCE_MODE to be disabled") + } + + if configParser.options.HasMaintenanceMode() { + t.Fatal("Expected HAS_MAINTENANCE_MODE to be disabled") + } +} + +func TestMediaProxyModeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MediaProxyMode() != "http-only" { + t.Fatalf("Expected MEDIA_PROXY_MODE to be 'http-only' by default") + } + + if err := configParser.parseLines([]string{"MEDIA_PROXY_MODE=all"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.MediaProxyMode() != "all" { + t.Fatalf("Expected MEDIA_PROXY_MODE to be 'all'") + } +} + +func TestMetricsCollectorOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MetricsCollector() { + t.Fatal("Expected METRICS_COLLECTOR to be disabled by default") + } + + if configParser.options.HasMetricsCollector() { + t.Fatal("Expected HAS_METRICS_COLLECTOR to be disabled by default") + } + + if err := configParser.parseLines([]string{"METRICS_COLLECTOR=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.MetricsCollector() { + t.Fatal("Expected METRICS_COLLECTOR to be enabled") + } + + if !configParser.options.HasMetricsCollector() { + t.Fatal("Expected HAS_METRICS_COLLECTOR to be enabled") + } + + if err := configParser.parseLines([]string{"METRICS_COLLECTOR=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.MetricsCollector() { + t.Fatal("Expected METRICS_COLLECTOR to be disabled") + } + + if configParser.options.HasMetricsCollector() { + t.Fatal("Expected HAS_METRICS_COLLECTOR to be disabled") + } +} + +func TestMetricsPasswordOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MetricsPassword() != "" { + t.Fatalf("Expected METRICS_PASSWORD to be empty by default") + } + + if err := configParser.parseLines([]string{"METRICS_PASSWORD=secret123"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.MetricsPassword() != "secret123" { + t.Fatalf("Expected METRICS_PASSWORD to be 'secret123'") + } +} + +func TestMetricsUsernameOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MetricsUsername() != "" { + t.Fatalf("Expected METRICS_USERNAME to be empty by default") + } + + if err := configParser.parseLines([]string{"METRICS_USERNAME=metrics_user"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.MetricsUsername() != "metrics_user" { + t.Fatalf("Expected METRICS_USERNAME to be 'metrics_user'") + } +} + +func TestOAuth2ClientIDOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.OAuth2ClientID() != "" { + t.Fatalf("Expected OAUTH2_CLIENT_ID to be empty by default") + } + + if err := configParser.parseLines([]string{"OAUTH2_CLIENT_ID=client123"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.OAuth2ClientID() != "client123" { + t.Fatalf("Expected OAUTH2_CLIENT_ID to be 'client123'") + } +} + +func TestOAuth2ClientSecretOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.OAuth2ClientSecret() != "" { + t.Fatalf("Expected OAUTH2_CLIENT_SECRET to be empty by default") + } + + if err := configParser.parseLines([]string{"OAUTH2_CLIENT_SECRET=secret456"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.OAuth2ClientSecret() != "secret456" { + t.Fatalf("Expected OAUTH2_CLIENT_SECRET to be 'secret456'") + } +} + +func TestOAuth2OIDCDiscoveryEndpointOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.OAuth2OIDCDiscoveryEndpoint() != "" { + t.Fatalf("Expected OAUTH2_OIDC_DISCOVERY_ENDPOINT to be empty by default") + } + + if err := configParser.parseLines([]string{"OAUTH2_OIDC_DISCOVERY_ENDPOINT=https://example.com/.well-known/openid_configuration"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.OAuth2OIDCDiscoveryEndpoint() != "https://example.com/.well-known/openid_configuration" { + t.Fatalf("Expected OAUTH2_OIDC_DISCOVERY_ENDPOINT to be 'https://example.com/.well-known/openid_configuration'") + } +} + +func TestOAuth2OIDCProviderNameOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.OAuth2OIDCProviderName() != "OpenID Connect" { + t.Fatalf("Expected OAUTH2_OIDC_PROVIDER_NAME to be 'OpenID Connect' by default") + } + + if err := configParser.parseLines([]string{"OAUTH2_OIDC_PROVIDER_NAME=My Provider"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.OAuth2OIDCProviderName() != "My Provider" { + t.Fatalf("Expected OAUTH2_OIDC_PROVIDER_NAME to be 'My Provider'") + } +} + +func TestOAuth2ProviderOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.OAuth2Provider() != "" { + t.Fatal("Expected OAUTH2_PROVIDER to be empty by default") + } + + if err := configParser.parseLines([]string{"OAUTH2_PROVIDER=google"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.OAuth2Provider() != "google" { + t.Fatal("Expected OAUTH2_PROVIDER to be 'google'") + } + + if err := configParser.parseLines([]string{"OAUTH2_PROVIDER=oidc"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.OAuth2Provider() != "oidc" { + t.Fatal("Expected OAUTH2_PROVIDER to be 'oidc'") + } + + if err := configParser.parseLines([]string{"OAUTH2_PROVIDER=invalid"}); err == nil { + t.Fatal("Expected error for invalid OAUTH2_PROVIDER value") + } +} + +func TestOAuth2RedirectURLOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.OAuth2RedirectURL() != "" { + t.Fatalf("Expected OAUTH2_REDIRECT_URL to be empty by default") + } + + if err := configParser.parseLines([]string{"OAUTH2_REDIRECT_URL=https://example.com/callback"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.OAuth2RedirectURL() != "https://example.com/callback" { + t.Fatalf("Expected OAUTH2_REDIRECT_URL to be 'https://example.com/callback'") + } +} + +func TestOAuth2UserCreationOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.OAuth2UserCreation() { + t.Fatal("Expected OAUTH2_USER_CREATION to be disabled by default") + } + + if configParser.options.IsOAuth2UserCreationAllowed() { + t.Fatal("Expected OAUTH2_USER_CREATION to be disabled by default") + } + + if err := configParser.parseLines([]string{"OAUTH2_USER_CREATION=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.OAuth2UserCreation() { + t.Fatal("Expected OAUTH2_USER_CREATION to be enabled") + } + + if !configParser.options.IsOAuth2UserCreationAllowed() { + t.Fatal("Expected OAUTH2_USER_CREATION to be enabled") + } + + if err := configParser.parseLines([]string{"OAUTH2_USER_CREATION=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.OAuth2UserCreation() { + t.Fatal("Expected OAUTH2_USER_CREATION to be disabled") + } + + if configParser.options.IsOAuth2UserCreationAllowed() { + t.Fatal("Expected OAUTH2_USER_CREATION to be disabled") + } +} + +func TestPollingLimitPerHostOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.PollingLimitPerHost() != 0 { + t.Fatalf("Expected POLLING_LIMIT_PER_HOST to be 0 by default") + } + + if err := configParser.parseLines([]string{"POLLING_LIMIT_PER_HOST=5"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.PollingLimitPerHost() != 5 { + t.Fatalf("Expected POLLING_LIMIT_PER_HOST to be 5") + } +} + +func TestPollingParsingErrorLimitOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.PollingParsingErrorLimit() != 3 { + t.Fatalf("Expected POLLING_PARSING_ERROR_LIMIT to be 3 by default") + } + + if err := configParser.parseLines([]string{"POLLING_PARSING_ERROR_LIMIT=5"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.PollingParsingErrorLimit() != 5 { + t.Fatalf("Expected POLLING_PARSING_ERROR_LIMIT to be 5") + } +} + +func TestPollingSchedulerOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.PollingScheduler() != "round_robin" { + t.Fatalf("Expected POLLING_SCHEDULER to be 'round_robin' by default") + } + + if err := configParser.parseLines([]string{"POLLING_SCHEDULER=entry_frequency"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.PollingScheduler() != "entry_frequency" { + t.Fatalf("Expected POLLING_SCHEDULER to be 'entry_frequency'") + } +} + +func TestPortOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.Port() != "" { + t.Fatalf("Expected PORT to be empty by default") + } + + if err := configParser.parseLines([]string{"PORT=1234"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.Port() != "1234" { + t.Fatalf("Expected PORT to be '1234'") + } + + addresses := configParser.options.ListenAddr() + if len(addresses) != 1 || addresses[0] != ":1234" { + t.Fatalf("Expected LISTEN_ADDR to be ':1234'") + } +} + +func TestRunMigrationsOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.RunMigrations() { + t.Fatalf("Expected RUN_MIGRATIONS to be disabled by default") + } + + if err := configParser.parseLines([]string{"RUN_MIGRATIONS=1"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.RunMigrations() { + t.Fatalf("Expected RUN_MIGRATIONS to be enabled") + } + + if err := configParser.parseLines([]string{"RUN_MIGRATIONS=0"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.RunMigrations() { + t.Fatalf("Expected RUN_MIGRATIONS to be disabled") + } +} + +func TestSchedulerEntryFrequencyFactorOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.SchedulerEntryFrequencyFactor() != 1 { + t.Fatalf("Expected SCHEDULER_ENTRY_FREQUENCY_FACTOR to be 1 by default") + } + + if err := configParser.parseLines([]string{"SCHEDULER_ENTRY_FREQUENCY_FACTOR=2"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.SchedulerEntryFrequencyFactor() != 2 { + t.Fatalf("Expected SCHEDULER_ENTRY_FREQUENCY_FACTOR to be 2") + } +} + +func TestYouTubeEmbedUrlOverrideOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + // Test default value + if configParser.options.YouTubeEmbedUrlOverride() != "https://www.youtube-nocookie.com/embed/" { + t.Fatal("Expected YOUTUBE_EMBED_URL_OVERRIDE to have default value") + } + + if configParser.options.YouTubeEmbedDomain() != "www.youtube-nocookie.com" { + t.Fatal("Expected YOUTUBE_EMBED_DOMAIN to be 'www.youtube-nocookie.com' by default") + } + + // Test custom value + if err := configParser.parseLines([]string{"YOUTUBE_EMBED_URL_OVERRIDE=https://custom.youtube.com/embed/"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.YouTubeEmbedUrlOverride() != "https://custom.youtube.com/embed/" { + t.Fatal("Expected YOUTUBE_EMBED_URL_OVERRIDE to be 'https://custom.youtube.com/embed/'") + } + + if configParser.options.YouTubeEmbedDomain() != "custom.youtube.com" { + t.Fatal("Expected YOUTUBE_EMBED_DOMAIN to be 'custom.youtube.com'") + } + + // Test empty value resets to default + configParser = NewConfigParser() + if err := configParser.parseLines([]string{"YOUTUBE_EMBED_URL_OVERRIDE="}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.YouTubeEmbedUrlOverride() != "https://www.youtube-nocookie.com/embed/" { + t.Fatal("Expected YOUTUBE_EMBED_URL_OVERRIDE to have default value") + } + + // Test invalid value + configParser = NewConfigParser() + if err := configParser.parseLines([]string{"YOUTUBE_EMBED_URL_OVERRIDE=http://example.com/%"}); err == nil { + t.Fatal("Expected error for invalid YOUTUBE_EMBED_URL_OVERRIDE") + } +} + +func TestCleanupArchiveReadIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CleanupArchiveReadInterval().Hours() != 24*60 { + t.Fatalf("Expected CLEANUP_ARCHIVE_READ_DAYS to be 60 days by default") + } + + if err := configParser.parseLines([]string{"CLEANUP_ARCHIVE_READ_DAYS=30"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CleanupArchiveReadInterval().Hours() != 24*30 { + t.Fatalf("Expected CLEANUP_ARCHIVE_READ_DAYS to be 30 days") + } +} + +func TestCleanupArchiveUnreadIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CleanupArchiveUnreadInterval().Hours() != 24*180 { + t.Fatalf("Expected CLEANUP_ARCHIVE_UNREAD_DAYS to be 180 days by default") + } + + if err := configParser.parseLines([]string{"CLEANUP_ARCHIVE_UNREAD_DAYS=90"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CleanupArchiveUnreadInterval().Hours() != 24*90 { + t.Fatalf("Expected CLEANUP_ARCHIVE_UNREAD_DAYS to be 90 days") + } +} + +func TestCleanupFrequencyOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CleanupFrequency().Hours() != 24 { + t.Fatalf("Expected CLEANUP_FREQUENCY_HOURS to be 24 hours by default") + } + + if err := configParser.parseLines([]string{"CLEANUP_FREQUENCY_HOURS=12"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CleanupFrequency().Hours() != 12 { + t.Fatalf("Expected CLEANUP_FREQUENCY_HOURS to be 12 hours") + } +} + +func TestCleanupRemoveSessionsIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.CleanupRemoveSessionsInterval().Hours() != 24*30 { + t.Fatalf("Expected CLEANUP_REMOVE_SESSIONS_DAYS to be 30 days by default") + } + + if err := configParser.parseLines([]string{"CLEANUP_REMOVE_SESSIONS_DAYS=14"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.CleanupRemoveSessionsInterval().Hours() != 24*14 { + t.Fatalf("Expected CLEANUP_REMOVE_SESSIONS_DAYS to be 14 days") + } +} + +func TestDatabaseConnectionLifetimeOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.DatabaseConnectionLifetime().Minutes() != 5 { + t.Fatalf("Expected DATABASE_CONNECTION_LIFETIME to be 5 minutes by default") + } + + if err := configParser.parseLines([]string{"DATABASE_CONNECTION_LIFETIME=10"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.DatabaseConnectionLifetime().Minutes() != 10 { + t.Fatalf("Expected DATABASE_CONNECTION_LIFETIME to be 10 minutes") + } +} + +func TestFilterEntryMaxAgeDaysOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.FilterEntryMaxAgeDays() != 0 { + t.Fatalf("Expected FILTER_ENTRY_MAX_AGE_DAYS to be 0 by default") + } + + if err := configParser.parseLines([]string{"FILTER_ENTRY_MAX_AGE_DAYS=7"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.FilterEntryMaxAgeDays() != 7 { + t.Fatalf("Expected FILTER_ENTRY_MAX_AGE_DAYS to be 7 days") + } +} + +func TestForceRefreshIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.ForceRefreshInterval().Minutes() != 30 { + t.Fatalf("Expected FORCE_REFRESH_INTERVAL to be 30 minutes by default") + } + + if err := configParser.parseLines([]string{"FORCE_REFRESH_INTERVAL=15"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.ForceRefreshInterval().Minutes() != 15 { + t.Fatalf("Expected FORCE_REFRESH_INTERVAL to be 15 minutes") + } +} + +func TestHTTPClientProxiesOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.HasHTTPClientProxiesConfigured() { + t.Fatalf("Expected HTTP_CLIENT_PROXIES to be empty by default") + } + + if err := configParser.parseLines([]string{"HTTP_CLIENT_PROXIES=proxy1.example.com:8080,proxy2.example.com:8080"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !configParser.options.HasHTTPClientProxiesConfigured() { + t.Fatalf("Expected HTTP_CLIENT_PROXIES to be configured") + } + + proxies := configParser.options.HTTPClientProxies() + if len(proxies) != 2 || proxies[0] != "proxy1.example.com:8080" || proxies[1] != "proxy2.example.com:8080" { + t.Fatalf("Expected HTTP_CLIENT_PROXIES to contain two proxies") + } +} + +func TestHTTPClientProxyOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.HTTPClientProxyURL() != nil { + t.Fatal("Expected HTTP_CLIENT_PROXY to be nil by default") + } + + if configParser.options.HasHTTPClientProxyURLConfigured() { + t.Fatal("Expected HAS_HTTP_CLIENT_PROXY to be disabled by default") + } + + if err := configParser.parseLines([]string{"HTTP_CLIENT_PROXY=http://proxy.example.com:8080"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + proxyURL := configParser.options.HTTPClientProxyURL() + if proxyURL == nil || proxyURL.String() != "http://proxy.example.com:8080" { + t.Fatal("Expected HTTP_CLIENT_PROXY to be 'http://proxy.example.com:8080'") + } + + if !configParser.options.HasHTTPClientProxyURLConfigured() { + t.Fatal("Expected HAS_HTTP_CLIENT_PROXY to be enabled") + } +} + +func TestHTTPClientTimeoutOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.HTTPClientTimeout().Seconds() != 20 { + t.Fatalf("Expected HTTP_CLIENT_TIMEOUT to be 20 seconds by default") + } + + if err := configParser.parseLines([]string{"HTTP_CLIENT_TIMEOUT=30"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.HTTPClientTimeout().Seconds() != 30 { + t.Fatalf("Expected HTTP_CLIENT_TIMEOUT to be 30 seconds") + } +} + +func TestHTTPServerTimeoutOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.HTTPServerTimeout().Seconds() != 300 { + t.Fatal("Expected HTTP_SERVER_TIMEOUT to be 300 seconds by default") + } + + if err := configParser.parseLines([]string{"HTTP_SERVER_TIMEOUT=60"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.HTTPServerTimeout().Seconds() != 60 { + t.Fatal("Expected HTTP_SERVER_TIMEOUT to be 60 seconds") + } +} + +func TestListenAddrOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + addrs := configParser.options.ListenAddr() + if len(addrs) != 1 || addrs[0] != "127.0.0.1:8080" { + t.Fatalf("Expected LISTEN_ADDR to be '127.0.0.1:8080' by default") + } + + if err := configParser.parseLines([]string{"LISTEN_ADDR=0.0.0.0:8080,127.0.0.1:8081,/unix.socket"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + addrs = configParser.options.ListenAddr() + if len(addrs) != 3 || addrs[0] != "0.0.0.0:8080" || addrs[1] != "127.0.0.1:8081" || addrs[2] != "/unix.socket" { + t.Fatalf("Expected LISTEN_ADDR to contain two addresses") + } +} + +func TestMediaCustomProxyURLOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MediaCustomProxyURL() != nil { + t.Fatalf("Expected MEDIA_PROXY_CUSTOM_URL to be nil by default") + } + + if err := configParser.parseLines([]string{"MEDIA_PROXY_CUSTOM_URL=https://proxy.example.com"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + proxyURL := configParser.options.MediaCustomProxyURL() + if proxyURL == nil || proxyURL.String() != "https://proxy.example.com" { + t.Fatalf("Expected MEDIA_PROXY_CUSTOM_URL to be 'https://proxy.example.com'") + } +} + +func TestMediaProxyHTTPClientTimeoutOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MediaProxyHTTPClientTimeout().Seconds() != 120 { + t.Fatalf("Expected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT to be 120 seconds by default") + } + + if err := configParser.parseLines([]string{"MEDIA_PROXY_HTTP_CLIENT_TIMEOUT=60"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.MediaProxyHTTPClientTimeout().Seconds() != 60 { + t.Fatalf("Expected MEDIA_PROXY_HTTP_CLIENT_TIMEOUT to be 60 seconds") + } +} + +func TestMediaProxyPrivateKeyOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if len(configParser.options.MediaProxyPrivateKey()) != 0 { + t.Fatalf("Expected MEDIA_PROXY_PRIVATE_KEY to be empty by default") + } + + if err := configParser.parseLines([]string{"MEDIA_PROXY_PRIVATE_KEY=secret123"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + privateKey := configParser.options.MediaProxyPrivateKey() + if string(privateKey) != "secret123" { + t.Fatalf("Expected MEDIA_PROXY_PRIVATE_KEY to be 'secret123'") + } +} + +func TestMediaProxyResourceTypesOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + resourceTypes := configParser.options.MediaProxyResourceTypes() + if len(resourceTypes) != 1 || resourceTypes[0] != "image" { + t.Fatalf("Expected MEDIA_PROXY_RESOURCE_TYPES to have default values") + } + + if err := configParser.parseLines([]string{"MEDIA_PROXY_RESOURCE_TYPES=image,video"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + resourceTypes = configParser.options.MediaProxyResourceTypes() + if len(resourceTypes) != 2 || resourceTypes[0] != "image" || resourceTypes[1] != "video" { + t.Fatalf("Expected MEDIA_PROXY_RESOURCE_TYPES to contain image and video") + } + + if err := configParser.parseLines([]string{"MEDIA_PROXY_RESOURCE_TYPES=image,invalid,video"}); err == nil { + t.Fatal("Expected error due to invalid resource type") + } +} + +func TestMetricsAllowedNetworksOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + networks := configParser.options.MetricsAllowedNetworks() + if len(networks) != 1 || networks[0] != "127.0.0.1/8" { + t.Fatalf("Expected METRICS_ALLOWED_NETWORKS to have default values") + } + + if err := configParser.parseLines([]string{"METRICS_ALLOWED_NETWORKS=10.0.0.0/8,192.168.0.0/16"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + networks = configParser.options.MetricsAllowedNetworks() + if len(networks) != 2 || networks[0] != "10.0.0.0/8" || networks[1] != "192.168.0.0/16" { + t.Fatalf("Expected METRICS_ALLOWED_NETWORKS to contain specified networks") + } +} + +func TestMetricsRefreshIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.MetricsRefreshInterval().Seconds() != 60 { + t.Fatalf("Expected METRICS_REFRESH_INTERVAL to be 60 seconds by default") + } + + if err := configParser.parseLines([]string{"METRICS_REFRESH_INTERVAL=120"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.MetricsRefreshInterval().Seconds() != 120 { + t.Fatalf("Expected METRICS_REFRESH_INTERVAL to be 120 seconds") + } +} + +func TestPollingFrequencyOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.PollingFrequency().Minutes() != 60 { + t.Fatalf("Expected POLLING_FREQUENCY to be 60 minutes by default") + } + + if err := configParser.parseLines([]string{"POLLING_FREQUENCY=30"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.PollingFrequency().Minutes() != 30 { + t.Fatalf("Expected POLLING_FREQUENCY to be 30 minutes") + } +} + +func TestSchedulerEntryFrequencyMaxIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.SchedulerEntryFrequencyMaxInterval().Hours() != 24 { + t.Fatalf("Expected SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL to be 24 hours by default") + } + + if err := configParser.parseLines([]string{"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL=720"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.SchedulerEntryFrequencyMaxInterval().Hours() != 12 { + t.Fatalf("Expected SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL to be 12 hours") + } +} + +func TestSchedulerEntryFrequencyMinIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.SchedulerEntryFrequencyMinInterval().Minutes() != 5 { + t.Fatalf("Expected SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL to be 5 minutes by default") + } + + if err := configParser.parseLines([]string{"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL=10"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.SchedulerEntryFrequencyMinInterval().Minutes() != 10 { + t.Fatalf("Expected SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL to be 10 minutes") + } +} + +func TestSchedulerRoundRobinMaxIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.SchedulerRoundRobinMaxInterval().Hours() != 24 { + t.Fatalf("Expected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL to be 24 hours by default") + } + + if err := configParser.parseLines([]string{"SCHEDULER_ROUND_ROBIN_MAX_INTERVAL=60"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.SchedulerRoundRobinMaxInterval().Hours() != 1 { + t.Fatalf("Expected SCHEDULER_ROUND_ROBIN_MAX_INTERVAL to be 1 hour") + } +} + +func TestSchedulerRoundRobinMinIntervalOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.SchedulerRoundRobinMinInterval().Minutes() != 60 { + t.Fatalf("Expected SCHEDULER_ROUND_ROBIN_MIN_INTERVAL to be 60 minutes by default") + } + + if err := configParser.parseLines([]string{"SCHEDULER_ROUND_ROBIN_MIN_INTERVAL=30"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.SchedulerRoundRobinMinInterval().Minutes() != 30 { + t.Fatalf("Expected SCHEDULER_ROUND_ROBIN_MIN_INTERVAL to be 30 minutes") + } +} + +func TestYouTubeEmbedDomainOptionParsing(t *testing.T) { + configParser := NewConfigParser() + + if configParser.options.YouTubeEmbedDomain() != "www.youtube-nocookie.com" { + t.Fatalf("Expected YouTubeEmbedDomain to be 'www.youtube-nocookie.com' by default") + } + + // YouTube embed domain is derived from YOUTUBE_EMBED_URL_OVERRIDE + if err := configParser.parseLines([]string{"YOUTUBE_EMBED_URL_OVERRIDE=https://custom.youtube.com/embed/"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if configParser.options.YouTubeEmbedDomain() != "custom.youtube.com" { + t.Fatalf("Expected YouTubeEmbedDomain to be 'custom.youtube.com'") + } +} + +func TestSetLogLevelFunction(t *testing.T) { + configParser := NewConfigParser() + + // Test default log level + if configParser.options.LogLevel() != "info" { + t.Fatalf("Expected LOG_LEVEL to be 'info' by default, got '%s'", configParser.options.LogLevel()) + } + + // Test setting log level to debug + configParser.options.SetLogLevel("debug") + if configParser.options.LogLevel() != "debug" { + t.Fatalf("Expected LOG_LEVEL to be 'debug' after SetLogLevel('debug'), got '%s'", configParser.options.LogLevel()) + } + if configParser.options.options["LOG_LEVEL"].RawValue != "debug" { + t.Fatalf("Expected LOG_LEVEL RawValue to be 'debug', got '%s'", configParser.options.options["LOG_LEVEL"].RawValue) + } + + // Test setting log level to warning + configParser.options.SetLogLevel("warning") + if configParser.options.LogLevel() != "warning" { + t.Fatalf("Expected LOG_LEVEL to be 'warning' after SetLogLevel('warning'), got '%s'", configParser.options.LogLevel()) + } + if configParser.options.options["LOG_LEVEL"].RawValue != "warning" { + t.Fatalf("Expected LOG_LEVEL RawValue to be 'warning', got '%s'", configParser.options.options["LOG_LEVEL"].RawValue) + } +} + +func TestSetHTTPSValueFunction(t *testing.T) { + configParser := NewConfigParser() + + // Test setting HTTPS to true + configParser.options.SetHTTPSValue(true) + if !configParser.options.HTTPS() { + t.Fatalf("Expected HTTPS to be true after SetHTTPSValue(true)") + } + + // Test setting HTTPS to false + configParser.options.SetHTTPSValue(false) + if configParser.options.HTTPS() { + t.Fatalf("Expected HTTPS to be false after SetHTTPSValue(false)") + } + + // Test setting HTTPS to true again + configParser.options.SetHTTPSValue(true) + if !configParser.options.HTTPS() { + t.Fatalf("Expected HTTPS to be true after second SetHTTPSValue(true)") + } +} + +func TestConfigMap(t *testing.T) { + configMap := NewConfigOptions().ConfigMap(false) + + if len(configMap) == 0 { + t.Fatal("Expected ConfigMap to contain configuration options") + } + + // The first option should be "ADMIN_PASSWORD" + if configMap[0].Key != "ADMIN_PASSWORD" { + t.Fatalf("Expected first config option to be 'ADMIN_PASSWORD', got '%s'", configMap[0].Key) + } +} + +func TestConfigMapWithRedactedSecrets(t *testing.T) { + configParser := NewConfigParser() + + if err := configParser.parseLines([]string{"ADMIN_PASSWORD=secret123"}); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + configMap := configParser.options.ConfigMap(true) + + if len(configMap) == 0 { + t.Fatal("Expected ConfigMap to contain configuration options") + } + + // The first option should be "ADMIN_PASSWORD" + if configMap[0].Key != "ADMIN_PASSWORD" { + t.Fatalf("Expected first config option to be 'ADMIN_PASSWORD', got '%s'", configMap[0].Key) + } + + // The value should be redacted + if configMap[0].Value != "" { + t.Fatalf("Expected ADMIN_PASSWORD value to be redacted, got '%s'", configMap[0].Value) + } +} diff --git a/internal/config/parser.go b/internal/config/parser.go index 752d96bb..0b0a36ee 100644 --- a/internal/config/parser.go +++ b/internal/config/parser.go @@ -7,7 +7,6 @@ import ( "bufio" "bytes" "crypto/rand" - "errors" "fmt" "io" "net/url" @@ -17,301 +16,205 @@ import ( "time" ) -// parser handles configuration parsing. -type parser struct { - opts *options +type configParser struct { + options *configOptions } -// NewParser returns a new Parser. -func NewParser() *parser { - return &parser{ - opts: NewOptions(), +func NewConfigParser() *configParser { + return &configParser{ + options: NewConfigOptions(), } } -// ParseEnvironmentVariables loads configuration values from environment variables. -func (p *parser) ParseEnvironmentVariables() (*options, error) { - err := p.parseLines(os.Environ()) - if err != nil { +func (cp *configParser) ParseEnvironmentVariables() (*configOptions, error) { + if err := cp.parseLines(os.Environ()); err != nil { return nil, err } - return p.opts, nil + + return cp.options, nil } -// ParseFile loads configuration values from a local file. -func (p *parser) ParseFile(filename string) (*options, error) { +func (cp *configParser) ParseFile(filename string) (*configOptions, error) { fp, err := os.Open(filename) if err != nil { return nil, err } defer fp.Close() - err = p.parseLines(p.parseFileContent(fp)) - if err != nil { + if err := cp.parseLines(parseFileContent(fp)); err != nil { return nil, err } - return p.opts, nil + + return cp.options, nil } -func (p *parser) parseFileContent(r io.Reader) (lines []string) { - scanner := bufio.NewScanner(r) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 { - lines = append(lines, line) - } - } - return lines -} +func (cp *configParser) postParsing() error { + // Parse basePath and rootURL based on BASE_URL + baseURL := cp.options.options["BASE_URL"].ParsedStringValue + baseURL = strings.TrimSuffix(baseURL, "/") -func (p *parser) parseLines(lines []string) (err error) { - var port string - - for lineNum, line := range lines { - key, value, ok := strings.Cut(line, "=") - if !ok { - return fmt.Errorf("config: unable to parse configuration, invalid format on line %d", lineNum) - } - key, value = strings.TrimSpace(key), strings.TrimSpace(value) - - switch key { - case "LOG_FILE": - p.opts.logFile = parseString(value, defaultLogFile) - case "LOG_DATE_TIME": - p.opts.logDateTime = parseBool(value, defaultLogDateTime) - case "LOG_LEVEL": - parsedValue := parseString(value, defaultLogLevel) - if parsedValue == "debug" || parsedValue == "info" || parsedValue == "warning" || parsedValue == "error" { - p.opts.logLevel = parsedValue - } - case "LOG_FORMAT": - parsedValue := parseString(value, defaultLogFormat) - if parsedValue == "json" || parsedValue == "text" { - p.opts.logFormat = parsedValue - } - 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 = parseStringList(value, []string{defaultListenAddr}) - case "DATABASE_URL": - p.opts.databaseURL = parseString(value, defaultDatabaseURL) - case "DATABASE_URL_FILE": - p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL) - case "DATABASE_MAX_CONNS": - p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns) - case "DATABASE_MIN_CONNS": - p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns) - case "DATABASE_CONNECTION_LIFETIME": - p.opts.databaseConnectionLifetime = parseInt(value, defaultDatabaseConnectionLifetime) - case "FILTER_ENTRY_MAX_AGE_DAYS": - p.opts.filterEntryMaxAgeDays = parseInt(value, defaultFilterEntryMaxAgeDays) - 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 "CLEANUP_FREQUENCY_HOURS": - p.opts.cleanupFrequencyInterval = parseInterval(value, time.Hour, defaultCleanupFrequency) - case "CLEANUP_ARCHIVE_READ_DAYS": - p.opts.cleanupArchiveReadInterval = parseInterval(value, 24*time.Hour, defaultCleanupArchiveReadInterval) - case "CLEANUP_ARCHIVE_UNREAD_DAYS": - p.opts.cleanupArchiveUnreadInterval = parseInterval(value, 24*time.Hour, defaultCleanupArchiveUnreadInterval) - case "CLEANUP_ARCHIVE_BATCH_SIZE": - p.opts.cleanupArchiveBatchSize = parseInt(value, defaultCleanupArchiveBatchSize) - case "CLEANUP_REMOVE_SESSIONS_DAYS": - p.opts.cleanupRemoveSessionsInterval = parseInterval(value, 24*time.Hour, defaultCleanupRemoveSessionsInterval) - case "WORKER_POOL_SIZE": - p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize) - case "FORCE_REFRESH_INTERVAL": - p.opts.forceRefreshInterval = parseInterval(value, time.Minute, defaultForceRefreshInterval) - case "BATCH_SIZE": - p.opts.batchSize = parseInt(value, defaultBatchSize) - case "POLLING_FREQUENCY": - p.opts.pollingFrequency = parseInterval(value, time.Minute, defaultPollingFrequency) - case "POLLING_LIMIT_PER_HOST": - p.opts.pollingLimitPerHost = parseInt(value, 0) - case "POLLING_PARSING_ERROR_LIMIT": - p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit) - case "POLLING_SCHEDULER": - p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler)) - case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": - p.opts.schedulerEntryFrequencyMaxInterval = parseInterval(value, time.Minute, defaultSchedulerEntryFrequencyMaxInterval) - case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": - p.opts.schedulerEntryFrequencyMinInterval = parseInterval(value, time.Minute, defaultSchedulerEntryFrequencyMinInterval) - case "SCHEDULER_ENTRY_FREQUENCY_FACTOR": - p.opts.schedulerEntryFrequencyFactor = parseInt(value, defaultSchedulerEntryFrequencyFactor) - case "SCHEDULER_ROUND_ROBIN_MIN_INTERVAL": - p.opts.schedulerRoundRobinMinInterval = parseInterval(value, time.Minute, defaultSchedulerRoundRobinMinInterval) - case "SCHEDULER_ROUND_ROBIN_MAX_INTERVAL": - p.opts.schedulerRoundRobinMaxInterval = parseInterval(value, time.Minute, defaultSchedulerRoundRobinMaxInterval) - case "MEDIA_PROXY_HTTP_CLIENT_TIMEOUT": - p.opts.mediaProxyHTTPClientTimeout = parseInterval(value, time.Second, defaultMediaProxyHTTPClientTimeout) - case "MEDIA_PROXY_MODE": - p.opts.mediaProxyMode = parseString(value, defaultMediaProxyMode) - case "MEDIA_PROXY_RESOURCE_TYPES": - p.opts.mediaProxyResourceTypes = parseStringList(value, []string{defaultMediaResourceTypes}) - case "MEDIA_PROXY_PRIVATE_KEY": - randomKey := make([]byte, 16) - rand.Read(randomKey) - p.opts.mediaProxyPrivateKey = parseBytes(value, randomKey) - case "MEDIA_PROXY_CUSTOM_URL": - p.opts.mediaProxyCustomURL, err = url.Parse(parseString(value, defaultMediaProxyURL)) - if err != nil { - return fmt.Errorf("config: invalid MEDIA_PROXY_CUSTOM_URL value: %w", err) - } - case "CREATE_ADMIN": - p.opts.createAdmin = parseBool(value, defaultCreateAdmin) - case "ADMIN_USERNAME": - p.opts.adminUsername = parseString(value, defaultAdminUsername) - case "ADMIN_USERNAME_FILE": - p.opts.adminUsername = readSecretFile(value, defaultAdminUsername) - case "ADMIN_PASSWORD": - p.opts.adminPassword = parseString(value, defaultAdminPassword) - case "ADMIN_PASSWORD_FILE": - p.opts.adminPassword = readSecretFile(value, defaultAdminPassword) - case "OAUTH2_USER_CREATION": - p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation) - case "OAUTH2_CLIENT_ID": - p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID) - case "OAUTH2_CLIENT_ID_FILE": - p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID) - case "OAUTH2_CLIENT_SECRET": - p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret) - case "OAUTH2_CLIENT_SECRET_FILE": - p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret) - case "OAUTH2_REDIRECT_URL": - p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL) - case "OAUTH2_OIDC_DISCOVERY_ENDPOINT": - p.opts.oidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint) - case "OAUTH2_OIDC_PROVIDER_NAME": - p.opts.oidcProviderName = parseString(value, defaultOauth2OidcProviderName) - case "OAUTH2_PROVIDER": - p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider) - case "DISABLE_LOCAL_AUTH": - p.opts.disableLocalAuth = parseBool(value, defaultDisableLocalAuth) - case "HTTP_CLIENT_TIMEOUT": - p.opts.httpClientTimeout = parseInterval(value, time.Second, defaultHTTPClientTimeout) - case "HTTP_CLIENT_MAX_BODY_SIZE": - p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024) - case "HTTP_CLIENT_PROXY": - p.opts.httpClientProxyURL, err = url.Parse(parseString(value, defaultHTTPClientProxy)) - if err != nil { - return fmt.Errorf("config: invalid HTTP_CLIENT_PROXY value: %w", err) - } - case "HTTP_CLIENT_PROXIES": - p.opts.httpClientProxies = parseStringList(value, []string{}) - case "HTTP_CLIENT_USER_AGENT": - p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent) - case "HTTP_SERVER_TIMEOUT": - p.opts.httpServerTimeout = parseInterval(value, time.Second, defaultHTTPServerTimeout) - case "AUTH_PROXY_HEADER": - p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader) - case "AUTH_PROXY_USER_CREATION": - p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation) - case "MAINTENANCE_MODE": - p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode) - case "MAINTENANCE_MESSAGE": - p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage) - case "METRICS_COLLECTOR": - p.opts.metricsCollector = parseBool(value, defaultMetricsCollector) - case "METRICS_REFRESH_INTERVAL": - p.opts.metricsRefreshInterval = parseInterval(value, time.Second, defaultMetricsRefreshInterval) - case "METRICS_ALLOWED_NETWORKS": - p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks}) - case "METRICS_USERNAME": - p.opts.metricsUsername = parseString(value, defaultMetricsUsername) - case "METRICS_USERNAME_FILE": - p.opts.metricsUsername = readSecretFile(value, defaultMetricsUsername) - case "METRICS_PASSWORD": - p.opts.metricsPassword = parseString(value, defaultMetricsPassword) - case "METRICS_PASSWORD_FILE": - p.opts.metricsPassword = readSecretFile(value, defaultMetricsPassword) - case "FETCH_BILIBILI_WATCH_TIME": - p.opts.fetchBilibiliWatchTime = parseBool(value, defaultFetchBilibiliWatchTime) - case "FETCH_NEBULA_WATCH_TIME": - p.opts.fetchNebulaWatchTime = parseBool(value, defaultFetchNebulaWatchTime) - case "FETCH_ODYSEE_WATCH_TIME": - p.opts.fetchOdyseeWatchTime = parseBool(value, defaultFetchOdyseeWatchTime) - case "FETCH_YOUTUBE_WATCH_TIME": - p.opts.fetchYouTubeWatchTime = parseBool(value, defaultFetchYouTubeWatchTime) - case "YOUTUBE_API_KEY": - p.opts.youTubeApiKey = parseString(value, defaultYouTubeApiKey) - case "YOUTUBE_EMBED_URL_OVERRIDE": - p.opts.youTubeEmbedUrlOverride = parseString(value, defaultYouTubeEmbedUrlOverride) - case "WATCHDOG": - p.opts.watchdog = parseBool(value, defaultWatchdog) - case "INVIDIOUS_INSTANCE": - p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance) - case "WEBAUTHN": - p.opts.webAuthn = parseBool(value, defaultWebAuthn) - } - } - - if port != "" { - p.opts.listenAddr = []string{":" + port} - } - - youtubeEmbedURL, err := url.Parse(p.opts.youTubeEmbedUrlOverride) + parsedURL, err := url.Parse(baseURL) if err != nil { - return fmt.Errorf("config: invalid YOUTUBE_EMBED_URL_OVERRIDE value: %w", err) - } - p.opts.youTubeEmbedDomain = youtubeEmbedURL.Hostname() - - return nil -} - -func parseBaseURL(value string) (string, string, string, error) { - if value == "" { - return defaultBaseURL, defaultRootURL, "", nil - } - - value = strings.TrimSuffix(value, "/") - - parsedURL, err := url.Parse(value) - if err != nil { - return "", "", "", fmt.Errorf("config: invalid BASE_URL: %w", err) + return fmt.Errorf("invalid BASE_URL: %v", err) } scheme := strings.ToLower(parsedURL.Scheme) if scheme != "https" && scheme != "http" { - return "", "", "", errors.New("config: invalid BASE_URL: scheme must be http or https") + return fmt.Errorf("BASE_URL scheme must be http or https") } - basePath := parsedURL.Path + cp.options.options["BASE_URL"].ParsedStringValue = baseURL + cp.options.basePath = parsedURL.Path + parsedURL.Path = "" - return value, parsedURL.String(), basePath, nil + cp.options.rootURL = parsedURL.String() + + // Parse YouTube embed domain based on YOUTUBE_EMBED_URL_OVERRIDE + youTubeEmbedURLOverride := cp.options.options["YOUTUBE_EMBED_URL_OVERRIDE"].ParsedStringValue + if youTubeEmbedURLOverride != "" { + parsedYouTubeEmbedURL, err := url.Parse(youTubeEmbedURLOverride) + if err != nil { + return fmt.Errorf("invalid YOUTUBE_EMBED_URL_OVERRIDE: %v", err) + } + cp.options.youTubeEmbedDomain = parsedYouTubeEmbedURL.Hostname() + } + + // Generate a media proxy private key if not set + if len(cp.options.options["MEDIA_PROXY_PRIVATE_KEY"].ParsedBytesValue) == 0 { + randomKey := make([]byte, 16) + rand.Read(randomKey) + cp.options.options["MEDIA_PROXY_PRIVATE_KEY"].ParsedBytesValue = randomKey + } + + // Override LISTEN_ADDR with PORT if set (for compatibility reasons) + if cp.options.Port() != "" { + cp.options.options["LISTEN_ADDR"].ParsedStringList = []string{":" + cp.options.Port()} + cp.options.options["LISTEN_ADDR"].RawValue = ":" + cp.options.Port() + } + + return nil } -func parseBool(value string, fallback bool) bool { +func (cp *configParser) parseLines(lines []string) error { + for lineNum, line := range lines { + key, value, ok := strings.Cut(line, "=") + if !ok { + return fmt.Errorf("unable to parse configuration, invalid format on line %d", lineNum) + } + + key, value = strings.TrimSpace(key), strings.TrimSpace(value) + if err := cp.parseLine(key, value); err != nil { + return err + } + } + + if err := cp.postParsing(); err != nil { + return err + } + + return nil +} + +func (cp *configParser) parseLine(key, value string) error { + field, exists := cp.options.options[key] + if !exists { + // Ignore unknown configuration keys to avoid parsing unrelated environment variables. + return nil + } + + // Validate the option if a validator is provided + if field.Validator != nil { + if err := field.Validator(value); err != nil { + return fmt.Errorf("invalid value for key %s: %v", key, err) + } + } + + // Convert the raw value based on its type + switch field.ValueType { + case stringType: + field.ParsedStringValue = parseStringValue(value, field.ParsedStringValue) + field.RawValue = value + case stringListType: + field.ParsedStringList = parseStringListValue(value, field.ParsedStringList) + field.RawValue = value + case boolType: + parsedValue, err := parseBoolValue(value, field.ParsedBoolValue) + if err != nil { + return fmt.Errorf("invalid boolean value for key %s: %v", key, err) + } + field.ParsedBoolValue = parsedValue + field.RawValue = value + case intType: + field.ParsedIntValue = parseIntValue(value, field.ParsedIntValue) + field.RawValue = value + case int64Type: + field.ParsedInt64Value = ParsedInt64Value(value, field.ParsedInt64Value) + field.RawValue = value + case secondType: + field.ParsedDuration = parseDurationValue(value, time.Second, field.ParsedDuration) + field.RawValue = value + case minuteType: + field.ParsedDuration = parseDurationValue(value, time.Minute, field.ParsedDuration) + field.RawValue = value + case hourType: + field.ParsedDuration = parseDurationValue(value, time.Hour, field.ParsedDuration) + field.RawValue = value + case dayType: + field.ParsedDuration = parseDurationValue(value, time.Hour*24, field.ParsedDuration) + field.RawValue = value + case urlType: + parsedURL, err := parseURLValue(value, field.ParsedURLValue) + if err != nil { + return fmt.Errorf("invalid URL for key %s: %v", key, err) + } + field.ParsedURLValue = parsedURL + field.RawValue = value + case secretFileType: + secretValue, err := readSecretFileValue(value) + if err != nil { + return fmt.Errorf("error reading secret file for key %s: %v", key, err) + } + if field.TargetKey != "" { + if targetField, ok := cp.options.options[field.TargetKey]; ok { + targetField.ParsedStringValue = secretValue + targetField.RawValue = secretValue + } + } + field.RawValue = value + case bytesType: + if value != "" { + field.ParsedBytesValue = []byte(value) + field.RawValue = value + } + } + + return nil +} + +func parseStringValue(value string, fallback string) string { if value == "" { return fallback } + return value +} + +func parseBoolValue(value string, fallback bool) (bool, error) { + if value == "" { + return fallback, nil + } value = strings.ToLower(value) if value == "1" || value == "yes" || value == "true" || value == "on" { - return true + return true, nil + } + if value == "0" || value == "no" || value == "false" || value == "off" { + return false, nil } - return false + return false, fmt.Errorf("invalid boolean value: %q", value) } -func parseInt(value string, fallback int) int { +func parseIntValue(value string, fallback int) int { if value == "" { return fallback } @@ -324,14 +227,20 @@ func parseInt(value string, fallback int) int { return v } -func parseString(value string, fallback string) string { +func ParsedInt64Value(value string, fallback int64) int64 { if value == "" { return fallback } - return value + + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return fallback + } + + return v } -func parseStringList(value string, fallback []string) []string { +func parseStringListValue(value string, fallback []string) []string { if value == "" { return fallback } @@ -351,16 +260,7 @@ func parseStringList(value string, fallback []string) []string { return strList } -func parseBytes(value string, fallback []byte) []byte { - if value == "" { - return fallback - } - - return []byte(value) -} - -// parseInterval converts an integer "value" to [time.Duration] using "unit" as multiplier. -func parseInterval(value string, unit time.Duration, fallback time.Duration) time.Duration { +func parseDurationValue(value string, unit time.Duration, fallback time.Duration) time.Duration { if value == "" { return fallback } @@ -373,16 +273,40 @@ func parseInterval(value string, unit time.Duration, fallback time.Duration) tim return time.Duration(v) * unit } -func readSecretFile(filename, fallback string) string { +func parseURLValue(value string, fallback *url.URL) (*url.URL, error) { + if value == "" { + return fallback, nil + } + + parsedURL, err := url.Parse(value) + if err != nil { + return fallback, err + } + + return parsedURL, nil +} + +func readSecretFileValue(filename string) (string, error) { data, err := os.ReadFile(filename) if err != nil { - return fallback + return "", err } value := string(bytes.TrimSpace(data)) if value == "" { - return fallback + return "", fmt.Errorf("secret file is empty") } - return value + return value, nil +} + +func parseFileContent(r io.Reader) (lines []string) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 { + lines = append(lines, line) + } + } + return lines } diff --git a/internal/config/parser_test.go b/internal/config/parser_test.go index fcde8228..2c16265f 100644 --- a/internal/config/parser_test.go +++ b/internal/config/parser_test.go @@ -4,163 +4,432 @@ package config // import "miniflux.app/v2/internal/config" import ( + "net/url" + "os" "reflect" "testing" + "time" ) -func TestParseBoolValue(t *testing.T) { - scenarios := map[string]bool{ - "": true, - "1": true, - "Yes": true, - "yes": true, - "True": true, - "true": true, - "on": true, - "false": false, - "off": false, - "invalid": false, +func TestParseStringValue(t *testing.T) { + // Test with non-empty value + result := parseStringValue("test", "fallback") + if result != "test" { + t.Errorf("Expected 'test', got '%s'", result) } - for input, expected := range scenarios { - result := parseBool(input, true) - if result != expected { - t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected) + // Test with empty value + result = parseStringValue("", "fallback") + if result != "fallback" { + t.Errorf("Expected 'fallback', got '%s'", result) + } + + // Test with empty value and empty fallback + result = parseStringValue("", "") + if result != "" { + t.Errorf("Expected empty string, got '%s'", result) + } +} + +func TestParseBoolValue(t *testing.T) { + // Test with empty value - should return fallback + result, err := parseBoolValue("", true) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != true { + t.Errorf("Expected true, got %v", result) + } + + // Test true values + trueValues := []string{"1", "yes", "true", "on", "YES", "TRUE", "ON"} + for _, value := range trueValues { + result, err := parseBoolValue(value, false) + if err != nil { + t.Errorf("Unexpected error for value '%s': %v", value, err) + } + if result != true { + t.Errorf("Expected true for '%s', got %v", value, result) } } -} -func TestParseStringValueWithUnsetVariable(t *testing.T) { - if parseString("", "defaultValue") != "defaultValue" { - t.Errorf(`Unset variables should returns the default value`) + // Test false values + falseValues := []string{"0", "no", "false", "off", "NO", "FALSE", "OFF"} + for _, value := range falseValues { + result, err := parseBoolValue(value, true) + if err != nil { + t.Errorf("Unexpected error for value '%s': %v", value, err) + } + if result != false { + t.Errorf("Expected false for '%s', got %v", value, result) + } } -} -func TestParseStringValue(t *testing.T) { - if parseString("test", "defaultValue") != "test" { - t.Errorf(`Defined variables should returns the specified value`) - } -} - -func TestParseIntValueWithUnsetVariable(t *testing.T) { - if parseInt("", 42) != 42 { - t.Errorf(`Unset variables should returns the default value`) - } -} - -func TestParseIntValueWithInvalidInput(t *testing.T) { - if parseInt("invalid integer", 42) != 42 { - t.Errorf(`Invalid integer should returns the default value`) + // Test invalid value - should return error + _, err = parseBoolValue("invalid", false) + if err == nil { + t.Error("Expected error for invalid boolean value") } } func TestParseIntValue(t *testing.T) { - if parseInt("2018", 42) != 2018 { - t.Errorf(`Defined variables should returns the specified value`) + // Test with empty value - should return fallback + result := parseIntValue("", 42) + if result != 42 { + t.Errorf("Expected 42, got %d", result) + } + + // Test with valid integer + result = parseIntValue("123", 42) + if result != 123 { + t.Errorf("Expected 123, got %d", result) + } + + // Test with invalid integer - should return fallback + result = parseIntValue("invalid", 42) + if result != 42 { + t.Errorf("Expected 42, got %d", result) + } + + // Test with zero + result = parseIntValue("0", 42) + if result != 0 { + t.Errorf("Expected 0, got %d", result) } } -func TestParseListenAddr(t *testing.T) { - defaultExpected := []string{defaultListenAddr} - - tests := []struct { - name string - listenAddr string - port string - expected []string - lines []string // Used for direct lines parsing instead of individual env vars - isLineOriented bool // Flag to indicate if we use lines - }{ - { - name: "Single LISTEN_ADDR", - listenAddr: "127.0.0.1:8080", - expected: []string{"127.0.0.1:8080"}, - }, - { - name: "Multiple LISTEN_ADDR comma-separated", - listenAddr: "127.0.0.1:8080,:8081,/tmp/miniflux.sock", - expected: []string{"127.0.0.1:8080", ":8081", "/tmp/miniflux.sock"}, - }, - { - name: "Multiple LISTEN_ADDR with spaces around commas", - listenAddr: "127.0.0.1:8080 , :8081", - expected: []string{"127.0.0.1:8080", ":8081"}, - }, - { - name: "Empty LISTEN_ADDR", - listenAddr: "", - expected: defaultExpected, - }, - { - name: "PORT overrides LISTEN_ADDR", - listenAddr: "127.0.0.1:8000", - port: "8082", - expected: []string{":8082"}, - }, - { - name: "PORT overrides empty LISTEN_ADDR", - listenAddr: "", - port: "8083", - expected: []string{":8083"}, - }, - { - name: "LISTEN_ADDR with empty segment (comma)", - listenAddr: "127.0.0.1:8080,,:8081", - expected: []string{"127.0.0.1:8080", ":8081"}, - }, - { - name: "PORT override with lines parsing", - isLineOriented: true, - lines: []string{"LISTEN_ADDR=127.0.0.1:8000", "PORT=8082"}, - expected: []string{":8082"}, - }, - { - name: "LISTEN_ADDR only with lines parsing (comma)", - isLineOriented: true, - lines: []string{"LISTEN_ADDR=10.0.0.1:9090,10.0.0.2:9091"}, - expected: []string{"10.0.0.1:9090", "10.0.0.2:9091"}, - }, - { - name: "Empty LISTEN_ADDR with lines parsing (default)", - isLineOriented: true, - lines: []string{"LISTEN_ADDR="}, - expected: defaultExpected, - }, +func TestParsedInt64Value(t *testing.T) { + // Test with empty value - should return fallback + result := ParsedInt64Value("", 42) + if result != 42 { + t.Errorf("Expected 42, got %d", result) } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewParser() - var err error + // Test with valid int64 + result = ParsedInt64Value("9223372036854775807", 42) + if result != 9223372036854775807 { + t.Errorf("Expected 9223372036854775807, got %d", result) + } - if tt.isLineOriented { - err = parser.parseLines(tt.lines) - } else { - // Simulate os.Environ() behaviour for individual var testing - var envLines []string - if tt.listenAddr != "" { - envLines = append(envLines, "LISTEN_ADDR="+tt.listenAddr) - } - if tt.port != "" { - envLines = append(envLines, "PORT="+tt.port) - } - // Add a dummy var if both are empty to avoid empty lines slice if not intended - if tt.listenAddr == "" && tt.port == "" && tt.name == "Empty LISTEN_ADDR" { - // This case specifically tests empty LISTEN_ADDR resulting in default - // So, we pass LISTEN_ADDR= - envLines = append(envLines, "LISTEN_ADDR=") - } - err = parser.parseLines(envLines) - } - - if err != nil { - t.Fatalf("parseLines() error = %v", err) - } - - opts := parser.opts - if !reflect.DeepEqual(opts.ListenAddr(), tt.expected) { - t.Errorf("ListenAddr() got = %v, want %v", opts.ListenAddr(), tt.expected) - } - }) + // Test with invalid int64 - should return fallback + result = ParsedInt64Value("invalid", 42) + if result != 42 { + t.Errorf("Expected 42, got %d", result) + } +} + +func TestParseStringListValue(t *testing.T) { + // Test with empty value - should return fallback + fallback := []string{"a", "b"} + result := parseStringListValue("", fallback) + if !reflect.DeepEqual(result, fallback) { + t.Errorf("Expected %v, got %v", fallback, result) + } + + // Test with single value + result = parseStringListValue("item1", nil) + expected := []string{"item1"} + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, got %v", expected, result) + } + + // Test with multiple values + result = parseStringListValue("item1,item2,item3", nil) + expected = []string{"item1", "item2", "item3"} + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, got %v", expected, result) + } + + // Test with duplicates - should remove duplicates + result = parseStringListValue("item1,item2,item1", nil) + expected = []string{"item1", "item2"} + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, got %v", expected, result) + } + + // Test with spaces + result = parseStringListValue(" item1 , item2 , item3 ", nil) + expected = []string{"item1", "item2", "item3"} + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, got %v", expected, result) + } +} + +func TestParseDurationValue(t *testing.T) { + // Test with empty value - should return fallback + fallback := 5 * time.Second + result := parseDurationValue("", time.Second, fallback) + if result != fallback { + t.Errorf("Expected %v, got %v", fallback, result) + } + + // Test with valid duration + result = parseDurationValue("30", time.Second, fallback) + expected := 30 * time.Second + if result != expected { + t.Errorf("Expected %v, got %v", expected, result) + } + + // Test with minutes + result = parseDurationValue("5", time.Minute, fallback) + expected = 5 * time.Minute + if result != expected { + t.Errorf("Expected %v, got %v", expected, result) + } + + // Test with invalid value - should return fallback + result = parseDurationValue("invalid", time.Second, fallback) + if result != fallback { + t.Errorf("Expected %v, got %v", fallback, result) + } +} + +func TestParseURLValue(t *testing.T) { + // Test with empty value - should return fallback + fallbackURL, _ := url.Parse("https://fallback.com") + result, err := parseURLValue("", fallbackURL) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result != fallbackURL { + t.Errorf("Expected %v, got %v", fallbackURL, result) + } + + // Test with valid URL + result, err = parseURLValue("https://example.com", nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result.String() != "https://example.com" { + t.Errorf("Expected https://example.com, got %s", result.String()) + } + + // Test with invalid URL - should return fallback and error + result, err = parseURLValue("://invalid", fallbackURL) + if err == nil { + t.Error("Expected error for invalid URL") + } + if result != fallbackURL { + t.Errorf("Expected fallback URL, got %v", result) + } +} + +func TestConfigFileParsing(t *testing.T) { + fileContent := ` + # This is a comment + LOG_FILE=miniflux.log + LOG_DATE_TIME=1 + LOG_FORMAT=json + LISTEN_ADDR=:8080,:8443 + ` + + // Write a temporary config file and parse it + tmpFile, err := os.CreateTemp("", "miniflux-*.txt") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + filename := tmpFile.Name() + if _, err := tmpFile.WriteString(fileContent); err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + + configParser := NewConfigParser() + configOptions, err := configParser.ParseFile(filename) + if err != nil { + t.Fatalf("Unexpected parsing error: %v", err) + } + + if configOptions.LogFile() != "miniflux.log" { + t.Fatalf("Unexpected log file, got %q", configOptions.LogFile()) + } + + if configOptions.LogDateTime() != true { + t.Fatalf("Unexpected log datetime, got %v", configOptions.LogDateTime()) + } + + if configOptions.LogFormat() != "json" { + t.Fatalf("Unexpected log format, got %q", configOptions.LogFormat()) + } + + if configOptions.LogLevel() != "info" { + t.Fatalf("Unexpected log level, got %q", configOptions.LogLevel()) + } + + if len(configOptions.ListenAddr()) != 2 || configOptions.ListenAddr()[0] != ":8080" || configOptions.ListenAddr()[1] != ":8443" { + t.Fatalf("Unexpected listen addresses, got %v", configOptions.ListenAddr()) + } +} + +func TestConfigFileParsingWithIncorrectKeyValuePair(t *testing.T) { + fileContent := ` + LOG_FILE=miniflux.log + INVALID_LINE + ` + + // Write a temporary config file and parse it + tmpFile, err := os.CreateTemp("", "miniflux-*.txt") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + filename := tmpFile.Name() + if _, err := tmpFile.WriteString(fileContent); err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + + configParser := NewConfigParser() + _, err = configParser.ParseFile(filename) + if err != nil { + t.Fatal("Invalid lines should be ignored, but got error:", err) + } +} + +func TestParseAdminPasswordFileOption(t *testing.T) { + tmpFile, err := os.CreateTemp("", "password-*.txt") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + password := "supersecret" + if _, err := tmpFile.WriteString(password); err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + + os.Clearenv() + os.Setenv("ADMIN_PASSWORD_FILE", tmpFile.Name()) + + configParser := NewConfigParser() + configOptions, err := configParser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf("Unexpected parsing error: %v", err) + } + + if configOptions.AdminPassword() != password { + t.Fatalf("Unexpected admin password, got %q", configOptions.AdminPassword()) + } +} + +func TestParseAdminPasswordFileOptionWithEmptyFile(t *testing.T) { + tmpFile, err := os.CreateTemp("", "empty-password-*.txt") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + os.Clearenv() + os.Setenv("ADMIN_PASSWORD_FILE", tmpFile.Name()) + + configParser := NewConfigParser() + _, err = configParser.ParseEnvironmentVariables() + if err == nil { + t.Fatal("Expected error due to empty password file, but got none") + } +} + +func TestParseLogFileOptionDefaultValue(t *testing.T) { + os.Clearenv() + + configParser := NewConfigParser() + configOptions, err := configParser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf("Unexpected parsing error: %v", err) + } + + if configOptions.LogFile() != "stderr" { + t.Fatalf("Unexpected default log file, got %q", configOptions.LogFile()) + } +} + +func TestParseLogFileOptionWithCustomFilename(t *testing.T) { + os.Clearenv() + os.Setenv("LOG_FILE", "miniflux.log") + + configParser := NewConfigParser() + configOptions, err := configParser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf("Unexpected parsing error: %v", err) + } + + if configOptions.LogFile() != "miniflux.log" { + t.Fatalf("Unexpected log file, got %q", configOptions.LogFile()) + } +} + +func TestParseLogFileOptionWithEmptyValue(t *testing.T) { + os.Clearenv() + os.Setenv("LOG_FILE", "") + + configParser := NewConfigParser() + configOptions, err := configParser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf("Unexpected parsing error: %v", err) + } + + if configOptions.LogFile() != "stderr" { + t.Fatalf("Unexpected log file, got %q", configOptions.LogFile()) + } +} + +func TestParseLogDateTimeOptionDefaultValue(t *testing.T) { + os.Clearenv() + + configParser := NewConfigParser() + configOptions, err := configParser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf("Unexpected parsing error: %v", err) + } + + if configOptions.LogDateTime() != false { + t.Fatalf("Unexpected default log datetime, got %v", configOptions.LogDateTime()) + } +} + +func TestParseLogDateTimeOptionWithCustomValue(t *testing.T) { + os.Clearenv() + os.Setenv("LOG_DATE_TIME", "true") + + configParser := NewConfigParser() + configOptions, err := configParser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf("Unexpected parsing error: %v", err) + } + + if configOptions.LogDateTime() != true { + t.Fatalf("Unexpected log datetime, got %v", configOptions.LogDateTime()) + } +} + +func TestParseLogDateTimeOptionWithEmptyValue(t *testing.T) { + os.Clearenv() + os.Setenv("LOG_DATE_TIME", "") + + configParser := NewConfigParser() + configOptions, err := configParser.ParseEnvironmentVariables() + if err != nil { + t.Fatalf("Unexpected parsing error: %v", err) + } + + if configOptions.LogDateTime() != false { + t.Fatalf("Unexpected log datetime, got %v", configOptions.LogDateTime()) + } +} + +func TestParseLogDateTimeOptionWithIncorrectValue(t *testing.T) { + os.Clearenv() + os.Setenv("LOG_DATE_TIME", "invalid") + + configParser := NewConfigParser() + if _, err := configParser.ParseEnvironmentVariables(); err == nil { + t.Fatal("Expected parsing error, got nil") } } diff --git a/internal/config/validators.go b/internal/config/validators.go new file mode 100644 index 00000000..381dc396 --- /dev/null +++ b/internal/config/validators.go @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config // import "miniflux.app/v2/internal/config" + +import ( + "fmt" + "slices" + "strconv" + "strings" +) + +func validateChoices(rawValue string, choices []string) error { + if !slices.Contains(choices, rawValue) { + return fmt.Errorf("value must be one of: %v", strings.Join(choices, ", ")) + } + return nil +} + +func validateListChoices(inputValues, choices []string) error { + for _, value := range inputValues { + if !slices.Contains(choices, value) { + return fmt.Errorf("value must be one of: %v", strings.Join(choices, ", ")) + } + } + return nil +} + +func validateGreaterThan(rawValue string, min int) error { + intValue, err := strconv.Atoi(rawValue) + if err != nil { + return fmt.Errorf("value must be an integer") + } + if intValue > min { + return nil + } + return fmt.Errorf("value must be at least %d", min) +} + +func validateGreaterOrEqualThan(rawValue string, min int) error { + intValue, err := strconv.Atoi(rawValue) + if err != nil { + return fmt.Errorf("value must be an integer") + } + if intValue >= min { + return nil + } + return fmt.Errorf("value must be greater or equal than %d", min) +} + +func validateRange(rawValue string, min, max int) error { + intValue, err := strconv.Atoi(rawValue) + if err != nil { + return fmt.Errorf("value must be an integer") + } + if intValue < min || intValue > max { + return fmt.Errorf("value must be between %d and %d", min, max) + } + return nil +} diff --git a/internal/config/validators_test.go b/internal/config/validators_test.go new file mode 100644 index 00000000..3212d630 --- /dev/null +++ b/internal/config/validators_test.go @@ -0,0 +1,372 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config // import "miniflux.app/v2/internal/config" + +import ( + "strings" + "testing" +) + +func TestValidateChoices(t *testing.T) { + tests := []struct { + name string + rawValue string + choices []string + expectError bool + }{ + { + name: "valid choice", + rawValue: "option1", + choices: []string{"option1", "option2", "option3"}, + expectError: false, + }, + { + name: "valid choice from middle", + rawValue: "option2", + choices: []string{"option1", "option2", "option3"}, + expectError: false, + }, + { + name: "valid choice from end", + rawValue: "option3", + choices: []string{"option1", "option2", "option3"}, + expectError: false, + }, + { + name: "invalid choice", + rawValue: "invalid", + choices: []string{"option1", "option2", "option3"}, + expectError: true, + }, + { + name: "empty value with non-empty choices", + rawValue: "", + choices: []string{"option1", "option2"}, + expectError: true, + }, + { + name: "case sensitive - different case", + rawValue: "OPTION1", + choices: []string{"option1", "option2"}, + expectError: true, + }, + { + name: "single choice valid", + rawValue: "only", + choices: []string{"only"}, + expectError: false, + }, + { + name: "empty choices list", + rawValue: "anything", + choices: []string{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateChoices(tt.rawValue, tt.choices) + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } else { + // Verify error message format + expectedPrefix := "value must be one of:" + if !strings.Contains(err.Error(), expectedPrefix) { + t.Errorf("error message should contain '%s', got: %s", expectedPrefix, err.Error()) + } + } + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +} + +func TestValidateListChoices(t *testing.T) { + tests := []struct { + name string + inputValues []string + choices []string + expectError bool + }{ + { + name: "all valid choices", + inputValues: []string{"option1", "option2"}, + choices: []string{"option1", "option2", "option3"}, + expectError: false, + }, + { + name: "single valid choice", + inputValues: []string{"option1"}, + choices: []string{"option1", "option2", "option3"}, + expectError: false, + }, + { + name: "empty input list", + inputValues: []string{}, + choices: []string{"option1", "option2", "option3"}, + expectError: false, + }, + { + name: "all choices from available list", + inputValues: []string{"option1", "option2", "option3"}, + choices: []string{"option1", "option2", "option3"}, + expectError: false, + }, + { + name: "duplicate valid choices", + inputValues: []string{"option1", "option1", "option2"}, + choices: []string{"option1", "option2", "option3"}, + expectError: false, + }, + { + name: "one invalid choice", + inputValues: []string{"option1", "invalid"}, + choices: []string{"option1", "option2", "option3"}, + expectError: true, + }, + { + name: "all invalid choices", + inputValues: []string{"invalid1", "invalid2"}, + choices: []string{"option1", "option2", "option3"}, + expectError: true, + }, + { + name: "case sensitive - different case", + inputValues: []string{"OPTION1"}, + choices: []string{"option1", "option2"}, + expectError: true, + }, + { + name: "empty string in input", + inputValues: []string{""}, + choices: []string{"option1", "option2"}, + expectError: true, + }, + { + name: "empty choices list with non-empty input", + inputValues: []string{"anything"}, + choices: []string{}, + expectError: true, + }, + { + name: "mixed valid and invalid choices", + inputValues: []string{"option1", "invalid", "option2"}, + choices: []string{"option1", "option2", "option3"}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateListChoices(tt.inputValues, tt.choices) + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } else { + // Verify error message format + expectedPrefix := "value must be one of:" + if !strings.Contains(err.Error(), expectedPrefix) { + t.Errorf("error message should contain '%s', got: %s", expectedPrefix, err.Error()) + } + } + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +} + +func TestValidateGreaterThan(t *testing.T) { + if err := validateGreaterThan("10", 5); err != nil { + t.Errorf("expected no error, got: %v", err) + } + + if err := validateGreaterThan("5", 5); err == nil { + t.Errorf("expected error, got none") + } + + if err := validateGreaterThan("abc", 5); err == nil { + t.Errorf("expected error for non-integer input, got none") + } + + if err := validateGreaterThan("-1", 0); err == nil { + t.Errorf("expected error for value below minimum, got none") + } +} + +func TestValidateGreaterOrEqualThan(t *testing.T) { + if err := validateGreaterOrEqualThan("10", 5); err != nil { + t.Errorf("expected no error, got: %v", err) + } + + if err := validateGreaterOrEqualThan("5", 5); err != nil { + t.Errorf("expected no error for equal value, got: %v", err) + } + + if err := validateGreaterOrEqualThan("abc", 5); err == nil { + t.Errorf("expected error for non-integer input, got none") + } + + if err := validateGreaterOrEqualThan("-1", 0); err == nil { + t.Errorf("expected error for value below minimum, got none") + } +} + +func TestValidateRange(t *testing.T) { + tests := []struct { + name string + rawValue string + min int + max int + expectError bool + errorMsg string + }{ + { + name: "valid integer within range", + rawValue: "5", + min: 1, + max: 10, + expectError: false, + }, + { + name: "valid integer at minimum", + rawValue: "1", + min: 1, + max: 10, + expectError: false, + }, + { + name: "valid integer at maximum", + rawValue: "10", + min: 1, + max: 10, + expectError: false, + }, + { + name: "valid zero in range", + rawValue: "0", + min: -5, + max: 5, + expectError: false, + }, + { + name: "valid negative in range", + rawValue: "-3", + min: -5, + max: 5, + expectError: false, + }, + { + name: "integer below minimum", + rawValue: "0", + min: 1, + max: 10, + expectError: true, + errorMsg: "value must be between 1 and 10", + }, + { + name: "integer above maximum", + rawValue: "11", + min: 1, + max: 10, + expectError: true, + errorMsg: "value must be between 1 and 10", + }, + { + name: "integer far below minimum", + rawValue: "-100", + min: 1, + max: 10, + expectError: true, + errorMsg: "value must be between 1 and 10", + }, + { + name: "integer far above maximum", + rawValue: "100", + min: 1, + max: 10, + expectError: true, + errorMsg: "value must be between 1 and 10", + }, + { + name: "non-integer string", + rawValue: "abc", + min: 1, + max: 10, + expectError: true, + errorMsg: "value must be an integer", + }, + { + name: "empty string", + rawValue: "", + min: 1, + max: 10, + expectError: true, + errorMsg: "value must be an integer", + }, + { + name: "float string", + rawValue: "5.5", + min: 1, + max: 10, + expectError: true, + errorMsg: "value must be an integer", + }, + { + name: "string with spaces", + rawValue: " 5 ", + min: 1, + max: 10, + expectError: true, + errorMsg: "value must be an integer", + }, + { + name: "single value range", + rawValue: "5", + min: 5, + max: 5, + expectError: false, + }, + { + name: "single value range - below", + rawValue: "4", + min: 5, + max: 5, + expectError: true, + errorMsg: "value must be between 5 and 5", + }, + { + name: "single value range - above", + rawValue: "6", + min: 5, + max: 5, + expectError: true, + errorMsg: "value must be between 5 and 5", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateRange(tt.rawValue, tt.min, tt.max) + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } else if tt.errorMsg != "" && err.Error() != tt.errorMsg { + t.Errorf("expected error message '%s', got '%s'", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +} diff --git a/internal/http/server/httpd.go b/internal/http/server/httpd.go index 32bf2275..839bae90 100644 --- a/internal/http/server/httpd.go +++ b/internal/http/server/httpd.go @@ -60,7 +60,7 @@ func StartWebServer(store *storage.Storage, pool *worker.Pool) []*http.Server { slog.Error("ACME HTTP challenge server failed", slog.Any("error", err)) } }() - config.Opts.HTTPS = true + config.Opts.SetHTTPSValue(true) httpServers = append(httpServers, challengeServer) } @@ -95,7 +95,7 @@ func StartWebServer(store *storage.Storage, pool *worker.Pool) []*http.Server { case certFile != "" && keyFile != "": server.Addr = listenAddr startTLSServer(server, certFile, keyFile) - config.Opts.HTTPS = true + config.Opts.SetHTTPSValue(true) default: server.Addr = listenAddr startHTTPServer(server) @@ -148,7 +148,7 @@ func startUnixSocketServer(server *http.Server, socketFile string) { slog.String("key_file", keyFile), ) // Ensure HTTPS is marked as true if any listener uses TLS - config.Opts.HTTPS = true + config.Opts.SetHTTPSValue(true) if err := server.ServeTLS(listener, certFile, keyFile); err != http.ErrServerClosed { printErrorAndExit("TLS Unix socket server failed to start on %s: %v", socketFile, err) } diff --git a/internal/http/server/middleware.go b/internal/http/server/middleware.go index 60b749b2..755a2c59 100644 --- a/internal/http/server/middleware.go +++ b/internal/http/server/middleware.go @@ -20,7 +20,7 @@ func middleware(next http.Handler) http.Handler { ctx = context.WithValue(ctx, request.ClientIPContextKey, clientIP) if r.Header.Get("X-Forwarded-Proto") == "https" { - config.Opts.HTTPS = true + config.Opts.SetHTTPSValue(true) } t1 := time.Now() @@ -36,7 +36,7 @@ func middleware(next http.Handler) http.Handler { ) }() - if config.Opts.HTTPS && config.Opts.HasHSTS() { + if config.Opts.HTTPS() && config.Opts.HasHSTS() { w.Header().Set("Strict-Transport-Security", "max-age=31536000") } diff --git a/internal/mediaproxy/media_proxy_test.go b/internal/mediaproxy/media_proxy_test.go index 83b7e7fc..6f724afc 100644 --- a/internal/mediaproxy/media_proxy_test.go +++ b/internal/mediaproxy/media_proxy_test.go @@ -19,7 +19,7 @@ func TestProxyFilterWithHttpDefault(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -43,7 +43,7 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) { os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -66,7 +66,7 @@ func TestProxyFilterWithHttpNever(t *testing.T) { os.Setenv("MEDIA_PROXY_MODE", "none") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -89,7 +89,7 @@ func TestProxyFilterWithHttpsNever(t *testing.T) { os.Setenv("MEDIA_PROXY_MODE", "none") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -114,7 +114,7 @@ func TestProxyFilterWithHttpAlways(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -139,7 +139,7 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -164,7 +164,7 @@ func TestAbsoluteProxyFilterWithHttpsAlways(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -188,7 +188,7 @@ func TestAbsoluteProxyFilterWithCustomPortAndSubfolderInBaseURL(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -226,7 +226,7 @@ func TestAbsoluteProxyFilterWithHttpsAlwaysAndAudioTag(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -251,7 +251,7 @@ func TestProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) { os.Setenv("MEDIA_PROXY_CUSTOM_URL", "https://proxy-example/proxy") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -276,7 +276,7 @@ func TestProxyFilterWithHttpsAlwaysAndIncorrectCustomProxyServer(t *testing.T) { os.Setenv("MEDIA_PROXY_CUSTOM_URL", "http://:8080example.com") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err == nil { t.Fatalf(`Incorrect proxy URL silently accepted (MEDIA_PROXY_CUSTOM_URL=%q): %q`, os.Getenv("MEDIA_PROXY_CUSTOM_URL"), config.Opts.MediaCustomProxyURL()) @@ -290,7 +290,7 @@ func TestAbsoluteProxyFilterWithHttpsAlwaysAndCustomProxyServer(t *testing.T) { os.Setenv("MEDIA_PROXY_CUSTOM_URL", "https://proxy-example/proxy") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -313,46 +313,8 @@ func TestProxyFilterWithHttpInvalid(t *testing.T) { os.Setenv("MEDIA_PROXY_MODE", "invalid") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") - 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/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := RewriteDocumentWithRelativeProxyURL(r, input) - expected := `

Test

` - - if expected != output { - t.Errorf(`Not expected output: got %q instead of %q`, output, expected) - } -} - -func TestProxyFilterWithHttpsInvalid(t *testing.T) { - os.Clearenv() - os.Setenv("MEDIA_PROXY_MODE", "invalid") - os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") - - 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/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy") - - input := `

Test

` - output := RewriteDocumentWithRelativeProxyURL(r, input) - expected := `

Test

` - - if expected != output { - t.Errorf(`Not expected output: got %q instead of %q`, output, expected) + if _, err := config.NewConfigParser().ParseEnvironmentVariables(); err == nil { + t.Fatalf(`Parsing should have failed (MEDIA_PROXY_MODE=%q): %q`, os.Getenv("MEDIA_PROXY_MODE"), config.Opts.MediaProxyMode()) } } @@ -363,7 +325,7 @@ func TestProxyFilterWithSrcset(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -388,7 +350,7 @@ func TestProxyFilterWithEmptySrcset(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -413,7 +375,7 @@ func TestProxyFilterWithPictureSource(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -433,12 +395,12 @@ func TestProxyFilterWithPictureSource(t *testing.T) { func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) { os.Clearenv() - os.Setenv("MEDIA_PROXY_MODE", "https") + os.Setenv("MEDIA_PROXY_MODE", "http-only") os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -462,7 +424,7 @@ func TestProxyWithImageDataURL(t *testing.T) { os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -486,7 +448,7 @@ func TestProxyWithImageSourceDataURL(t *testing.T) { os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -511,7 +473,7 @@ func TestProxyFilterWithVideo(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -536,7 +498,7 @@ func TestProxyFilterVideoPoster(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -561,7 +523,7 @@ func TestProxyFilterVideoPosterOnce(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) diff --git a/internal/model/enclosure_test.go b/internal/model/enclosure_test.go index bea1420b..9bf5f4ca 100644 --- a/internal/model/enclosure_test.go +++ b/internal/model/enclosure_test.go @@ -290,7 +290,7 @@ func TestEnclosure_ProxifyEnclosureURL(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Config parsing failure: %v`, err) @@ -420,7 +420,7 @@ func TestEnclosureList_ProxifyEnclosureURL(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Config parsing failure: %v`, err) @@ -535,7 +535,7 @@ func TestEnclosure_ProxifyEnclosureURL_EdgeCases(t *testing.T) { os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Config parsing failure: %v`, err) diff --git a/internal/model/feed_test.go b/internal/model/feed_test.go index 6cc7bc59..b5094870 100644 --- a/internal/model/feed_test.go +++ b/internal/model/feed_test.go @@ -80,7 +80,7 @@ func TestFeedScheduleNextCheckRoundRobinDefault(t *testing.T) { os.Clearenv() var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -102,7 +102,7 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMinInterval(t *test os.Clearenv() var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -125,7 +125,7 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval(t *test os.Clearenv() var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -148,7 +148,7 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval(t *test os.Clearenv() var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -174,7 +174,7 @@ func TestFeedScheduleNextCheckRoundRobinMinInterval(t *testing.T) { os.Setenv("SCHEDULER_ROUND_ROBIN_MIN_INTERVAL", strconv.Itoa(minInterval)) var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -201,7 +201,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxInterval(t *testing.T) { os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval)) var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -230,7 +230,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxIntervalZeroWeeklyCount(t *testin os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval)) var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -259,7 +259,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMinInterval(t *testing.T) { os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval)) var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -286,7 +286,7 @@ func TestFeedScheduleNextCheckEntryFrequencyFactor(t *testing.T) { os.Setenv("SCHEDULER_ENTRY_FREQUENCY_FACTOR", strconv.Itoa(factor)) var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -315,7 +315,7 @@ func TestFeedScheduleNextCheckEntryFrequencySmallNewTTL(t *testing.T) { os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval)) var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) @@ -351,7 +351,7 @@ func TestFeedScheduleNextCheckEntryFrequencyLargeNewTTL(t *testing.T) { os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval)) var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) diff --git a/internal/reader/filter/filter_test.go b/internal/reader/filter/filter_test.go index d485689b..09b1574b 100644 --- a/internal/reader/filter/filter_test.go +++ b/internal/reader/filter/filter_test.go @@ -414,7 +414,7 @@ func TestKeeplistRulesBehavior(t *testing.T) { // Tests for isBlockedGlobally function func TestIsBlockedGlobally(t *testing.T) { var err error - config.Opts, err = config.NewParser().ParseEnvironmentVariables() + config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) } @@ -429,7 +429,7 @@ func TestIsBlockedGlobally(t *testing.T) { os.Setenv("FILTER_ENTRY_MAX_AGE_DAYS", "30") defer os.Clearenv() - config.Opts, err = config.NewParser().ParseEnvironmentVariables() + config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) } diff --git a/internal/reader/processor/youtube.go b/internal/reader/processor/youtube.go index 68aeec62..8e04bbe8 100644 --- a/internal/reader/processor/youtube.go +++ b/internal/reader/processor/youtube.go @@ -31,11 +31,11 @@ func getVideoIDFromYouTubeURL(websiteURL string) string { } func shouldFetchYouTubeWatchTimeForSingleEntry(entry *model.Entry) bool { - return config.Opts.FetchYouTubeWatchTime() && config.Opts.YouTubeApiKey() == "" && isYouTubeVideoURL(entry.URL) + return config.Opts.FetchYouTubeWatchTime() && config.Opts.YouTubeAPIKey() == "" && isYouTubeVideoURL(entry.URL) } func shouldFetchYouTubeWatchTimeInBulk() bool { - return config.Opts.FetchYouTubeWatchTime() && config.Opts.YouTubeApiKey() != "" + return config.Opts.FetchYouTubeWatchTime() && config.Opts.YouTubeAPIKey() != "" } func fetchYouTubeWatchTimeForSingleEntry(websiteURL string) (int, error) { @@ -82,7 +82,7 @@ func fetchYouTubeWatchTimeFromApiInBulk(videoIDs []string) (map[string]time.Dura apiQuery := url.Values{} apiQuery.Set("id", strings.Join(videoIDs, ",")) - apiQuery.Set("key", config.Opts.YouTubeApiKey()) + apiQuery.Set("key", config.Opts.YouTubeAPIKey()) apiQuery.Set("part", "contentDetails") apiURL := url.URL{ diff --git a/internal/reader/rewrite/content_rewrite_test.go b/internal/reader/rewrite/content_rewrite_test.go index 40d624c8..307dd747 100644 --- a/internal/reader/rewrite/content_rewrite_test.go +++ b/internal/reader/rewrite/content_rewrite_test.go @@ -67,7 +67,7 @@ func TestRewriteWithNoMatchingRule(t *testing.T) { } func TestRewriteYoutubeVideoLink(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() controlEntry := &model.Entry{ URL: "https://www.youtube.com/watch?v=1234", @@ -87,7 +87,7 @@ func TestRewriteYoutubeVideoLink(t *testing.T) { } func TestRewriteYoutubeShortLink(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() controlEntry := &model.Entry{ URL: "https://www.youtube.com/shorts/1LUWKWZkPjo", @@ -107,7 +107,7 @@ func TestRewriteYoutubeShortLink(t *testing.T) { } func TestRewriteIncorrectYoutubeLink(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() controlEntry := &model.Entry{ URL: "https://www.youtube.com/some-page", @@ -131,7 +131,7 @@ func TestRewriteYoutubeLinkAndCustomEmbedURL(t *testing.T) { os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { @@ -156,7 +156,7 @@ func TestRewriteYoutubeLinkAndCustomEmbedURL(t *testing.T) { } func TestRewriteYoutubeVideoLinkUsingInvidious(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() controlEntry := &model.Entry{ URL: "https://www.youtube.com/watch?v=1234", Title: `A title`, @@ -176,7 +176,7 @@ func TestRewriteYoutubeVideoLinkUsingInvidious(t *testing.T) { } func TestRewriteYoutubeShortLinkUsingInvidious(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() controlEntry := &model.Entry{ URL: "https://www.youtube.com/shorts/1LUWKWZkPjo", Title: `A title`, @@ -196,7 +196,7 @@ func TestRewriteYoutubeShortLinkUsingInvidious(t *testing.T) { } func TestAddYoutubeVideoFromId(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() scenarios := map[string]string{ // Test with single YouTube ID @@ -239,7 +239,7 @@ func TestAddYoutubeVideoFromIdWithCustomEmbedURL(t *testing.T) { os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") var err error - parser := config.NewParser() + parser := config.NewConfigParser() config.Opts, err = parser.ParseEnvironmentVariables() if err != nil { diff --git a/internal/reader/sanitizer/sanitizer_test.go b/internal/reader/sanitizer/sanitizer_test.go index faa86b2f..4772496b 100644 --- a/internal/reader/sanitizer/sanitizer_test.go +++ b/internal/reader/sanitizer/sanitizer_test.go @@ -392,7 +392,7 @@ func TestInvalidNestedTag(t *testing.T) { } func TestInvalidIFrame(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() input := `` expected := `` @@ -404,7 +404,7 @@ func TestInvalidIFrame(t *testing.T) { } func TestSameDomainIFrame(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() input := `` expected := `` @@ -416,7 +416,7 @@ func TestSameDomainIFrame(t *testing.T) { } func TestInvidiousIFrame(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() input := `` expected := `` @@ -432,7 +432,7 @@ func TestCustomYoutubeEmbedURL(t *testing.T) { defer os.Clearenv() var err error - if config.Opts, err = config.NewParser().ParseEnvironmentVariables(); err != nil { + if config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables(); err != nil { t.Fatalf(`Parsing failure: %v`, err) } @@ -446,7 +446,7 @@ func TestCustomYoutubeEmbedURL(t *testing.T) { } func TestIFrameWithChildElements(t *testing.T) { - config.Opts = config.NewOptions() + config.Opts = config.NewConfigOptions() input := `` expected := `` @@ -850,7 +850,7 @@ func TestReplaceYoutubeURLWithCustomURL(t *testing.T) { os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") var err error - config.Opts, err = config.NewParser().ParseEnvironmentVariables() + config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables() if err != nil { t.Fatalf(`Parsing failure: %v`, err) } diff --git a/internal/template/functions.go b/internal/template/functions.go index 2a478cc5..59184ded 100644 --- a/internal/template/functions.go +++ b/internal/template/functions.go @@ -41,7 +41,7 @@ func (f *funcMap) Map() template.FuncMap { "baseURL": config.Opts.BaseURL, "rootURL": config.Opts.RootURL, "disableLocalAuth": config.Opts.DisableLocalAuth, - "oidcProviderName": config.Opts.OIDCProviderName, + "oidcProviderName": config.Opts.OAuth2OIDCProviderName, "hasOAuth2Provider": func(provider string) bool { return config.Opts.OAuth2Provider() == provider }, diff --git a/internal/ui/about.go b/internal/ui/about.go index d22ee32d..7d3596cf 100644 --- a/internal/ui/about.go +++ b/internal/ui/about.go @@ -33,7 +33,7 @@ func (h *handler) showAboutPage(w http.ResponseWriter, r *http.Request) { view.Set("user", user) view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID)) - view.Set("globalConfigOptions", config.Opts.SortedOptions(true)) + view.Set("globalConfigOptions", config.Opts.ConfigMap(true)) view.Set("postgres_version", h.store.DatabaseVersion()) view.Set("go_version", runtime.Version()) diff --git a/internal/ui/login_check.go b/internal/ui/login_check.go index eb3c6720..2c3703ef 100644 --- a/internal/ui/login_check.go +++ b/internal/ui/login_check.go @@ -89,7 +89,7 @@ func (h *handler) checkLogin(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, cookie.New( cookie.CookieUserSessionID, sessionToken, - config.Opts.HTTPS, + config.Opts.HTTPS(), config.Opts.BasePath(), )) diff --git a/internal/ui/logout.go b/internal/ui/logout.go index 92896050..5272bd4a 100644 --- a/internal/ui/logout.go +++ b/internal/ui/logout.go @@ -32,7 +32,7 @@ func (h *handler) logout(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, cookie.Expired( cookie.CookieUserSessionID, - config.Opts.HTTPS, + config.Opts.HTTPS(), config.Opts.BasePath(), )) diff --git a/internal/ui/middleware.go b/internal/ui/middleware.go index 0ee36b9d..6e37f916 100644 --- a/internal/ui/middleware.go +++ b/internal/ui/middleware.go @@ -93,7 +93,7 @@ func (m *middleware) handleAppSession(next http.Handler) http.Handler { } } - http.SetCookie(w, cookie.New(cookie.CookieAppSessionID, session.ID, config.Opts.HTTPS, config.Opts.BasePath())) + http.SetCookie(w, cookie.New(cookie.CookieAppSessionID, session.ID, config.Opts.HTTPS(), config.Opts.BasePath())) } if r.Method == http.MethodPost { @@ -261,7 +261,7 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler { http.SetCookie(w, cookie.New( cookie.CookieUserSessionID, sessionToken, - config.Opts.HTTPS, + config.Opts.HTTPS(), config.Opts.BasePath(), )) diff --git a/internal/ui/oauth2.go b/internal/ui/oauth2.go index b6547f95..4f7d3e08 100644 --- a/internal/ui/oauth2.go +++ b/internal/ui/oauth2.go @@ -16,6 +16,6 @@ func getOAuth2Manager(ctx context.Context) *oauth2.Manager { config.Opts.OAuth2ClientID(), config.Opts.OAuth2ClientSecret(), config.Opts.OAuth2RedirectURL(), - config.Opts.OIDCDiscoveryEndpoint(), + config.Opts.OAuth2OIDCDiscoveryEndpoint(), ) } diff --git a/internal/ui/oauth2_callback.go b/internal/ui/oauth2_callback.go index 3d649853..5c7f6a63 100644 --- a/internal/ui/oauth2_callback.go +++ b/internal/ui/oauth2_callback.go @@ -145,7 +145,7 @@ func (h *handler) oauth2Callback(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, cookie.New( cookie.CookieUserSessionID, sessionToken, - config.Opts.HTTPS, + config.Opts.HTTPS(), config.Opts.BasePath(), )) diff --git a/internal/ui/webauthn.go b/internal/ui/webauthn.go index 7525d3f3..469050ab 100644 --- a/internal/ui/webauthn.go +++ b/internal/ui/webauthn.go @@ -331,7 +331,7 @@ func (h *handler) finishLogin(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, cookie.New( cookie.CookieUserSessionID, sessionToken, - config.Opts.HTTPS, + config.Opts.HTTPS(), config.Opts.BasePath(), )) diff --git a/miniflux.1 b/miniflux.1 index 374e5258..fe059cf7 100644 --- a/miniflux.1 +++ b/miniflux.1 @@ -329,7 +329,7 @@ Default is 300 seconds\&. .B HTTPS Forces cookies to use secure flag and send HSTS header\&. .br -Default is empty\&. +Default is disabled\&. .TP .B INVIDIOUS_INSTANCE Set a custom invidious instance to use\&. @@ -466,7 +466,7 @@ Default is empty\&. .B OAUTH2_OIDC_PROVIDER_NAME Name to display for the OIDC provider\&. .br -Default is OpenID Connect\&. +Default is "OpenID Connect"\&. .TP .B OAUTH2_PROVIDER Possible values are "google" or "oidc"\&. @@ -537,7 +537,7 @@ Default is 1\&. .B SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL Maximum interval in minutes for the entry frequency scheduler\&. .br -Default is 24 hours\&. +Default is 1440 minutes (24 hours)\&. .TP .B SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL Minimum interval in minutes for the entry frequency scheduler\&.