1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-09-15 18:57:04 +00:00

refactor(config): rewrite config parser

This PR refactors the configuration parser, replacing the old parser implementation with a new, more structured approach that includes validation and improved organization.

Key changes:
- Complete rewrite of the configuration parser using a map-based structure with built-in validation
- Addition of comprehensive validator functions for configuration values
- Renamed numerous configuration getter methods for better consistency
This commit is contained in:
Frédéric Guillot 2025-09-14 10:51:04 -07:00 committed by GitHub
parent 502e7108dd
commit 5e607be86a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 3615 additions and 3523 deletions

View file

@ -78,7 +78,7 @@ func Parse() {
flag.StringVar(&flagExportUserFeeds, "export-user-feeds", "", flagExportUserFeedsHelp) flag.StringVar(&flagExportUserFeeds, "export-user-feeds", "", flagExportUserFeedsHelp)
flag.Parse() flag.Parse()
cfg := config.NewParser() cfg := config.NewConfigParser()
if flagConfigFile != "" { if flagConfigFile != "" {
config.Opts, err = cfg.ParseFile(flagConfigFile) config.Opts, err = cfg.ParseFile(flagConfigFile)

11
internal/config/config.go Normal file
View file

@ -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)"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,6 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"errors"
"fmt" "fmt"
"io" "io"
"net/url" "net/url"
@ -17,301 +16,205 @@ import (
"time" "time"
) )
// parser handles configuration parsing. type configParser struct {
type parser struct { options *configOptions
opts *options
} }
// NewParser returns a new Parser. func NewConfigParser() *configParser {
func NewParser() *parser { return &configParser{
return &parser{ options: NewConfigOptions(),
opts: NewOptions(),
} }
} }
// ParseEnvironmentVariables loads configuration values from environment variables. func (cp *configParser) ParseEnvironmentVariables() (*configOptions, error) {
func (p *parser) ParseEnvironmentVariables() (*options, error) { if err := cp.parseLines(os.Environ()); err != nil {
err := p.parseLines(os.Environ())
if err != nil {
return nil, err return nil, err
} }
return p.opts, nil
return cp.options, nil
} }
// ParseFile loads configuration values from a local file. func (cp *configParser) ParseFile(filename string) (*configOptions, error) {
func (p *parser) ParseFile(filename string) (*options, error) {
fp, err := os.Open(filename) fp, err := os.Open(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer fp.Close() defer fp.Close()
err = p.parseLines(p.parseFileContent(fp)) if err := cp.parseLines(parseFileContent(fp)); err != nil {
if err != nil {
return nil, err return nil, err
} }
return p.opts, nil
return cp.options, nil
} }
func (p *parser) parseFileContent(r io.Reader) (lines []string) { func (cp *configParser) postParsing() error {
scanner := bufio.NewScanner(r) // Parse basePath and rootURL based on BASE_URL
for scanner.Scan() { baseURL := cp.options.options["BASE_URL"].ParsedStringValue
line := strings.TrimSpace(scanner.Text()) baseURL = strings.TrimSuffix(baseURL, "/")
if !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
lines = append(lines, line)
}
}
return lines
}
func (p *parser) parseLines(lines []string) (err error) { parsedURL, err := url.Parse(baseURL)
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 { if err != nil {
return err return fmt.Errorf("invalid BASE_URL: %v", 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)
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)
} }
scheme := strings.ToLower(parsedURL.Scheme) scheme := strings.ToLower(parsedURL.Scheme)
if scheme != "https" && scheme != "http" { 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 = "" 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()
} }
func parseBool(value string, fallback bool) bool { // 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 (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 == "" { if value == "" {
return fallback return fallback
} }
return value
}
func parseBoolValue(value string, fallback bool) (bool, error) {
if value == "" {
return fallback, nil
}
value = strings.ToLower(value) value = strings.ToLower(value)
if value == "1" || value == "yes" || value == "true" || value == "on" { 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 == "" { if value == "" {
return fallback return fallback
} }
@ -324,14 +227,20 @@ func parseInt(value string, fallback int) int {
return v return v
} }
func parseString(value string, fallback string) string { func ParsedInt64Value(value string, fallback int64) int64 {
if value == "" { if value == "" {
return fallback return fallback
} }
return value
v, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fallback
} }
func parseStringList(value string, fallback []string) []string { return v
}
func parseStringListValue(value string, fallback []string) []string {
if value == "" { if value == "" {
return fallback return fallback
} }
@ -351,16 +260,7 @@ func parseStringList(value string, fallback []string) []string {
return strList return strList
} }
func parseBytes(value string, fallback []byte) []byte { func parseDurationValue(value string, unit time.Duration, fallback time.Duration) time.Duration {
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 {
if value == "" { if value == "" {
return fallback return fallback
} }
@ -373,16 +273,40 @@ func parseInterval(value string, unit time.Duration, fallback time.Duration) tim
return time.Duration(v) * unit 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) data, err := os.ReadFile(filename)
if err != nil { if err != nil {
return fallback return "", err
} }
value := string(bytes.TrimSpace(data)) value := string(bytes.TrimSpace(data))
if value == "" { 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
} }

View file

@ -4,163 +4,432 @@
package config // import "miniflux.app/v2/internal/config" package config // import "miniflux.app/v2/internal/config"
import ( import (
"net/url"
"os"
"reflect" "reflect"
"testing" "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,
}
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)
}
}
}
func TestParseStringValueWithUnsetVariable(t *testing.T) {
if parseString("", "defaultValue") != "defaultValue" {
t.Errorf(`Unset variables should returns the default value`)
}
}
func TestParseStringValue(t *testing.T) { func TestParseStringValue(t *testing.T) {
if parseString("test", "defaultValue") != "test" { // Test with non-empty value
t.Errorf(`Defined variables should returns the specified value`) result := parseStringValue("test", "fallback")
if result != "test" {
t.Errorf("Expected 'test', got '%s'", result)
}
// 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 TestParseIntValueWithUnsetVariable(t *testing.T) { func TestParseBoolValue(t *testing.T) {
if parseInt("", 42) != 42 { // Test with empty value - should return fallback
t.Errorf(`Unset variables should returns the default value`) 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 TestParseIntValueWithInvalidInput(t *testing.T) { // Test false values
if parseInt("invalid integer", 42) != 42 { falseValues := []string{"0", "no", "false", "off", "NO", "FALSE", "OFF"}
t.Errorf(`Invalid integer should returns the default value`) 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)
}
}
// 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) { func TestParseIntValue(t *testing.T) {
if parseInt("2018", 42) != 2018 { // Test with empty value - should return fallback
t.Errorf(`Defined variables should returns the specified value`) 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) { func TestParsedInt64Value(t *testing.T) {
defaultExpected := []string{defaultListenAddr} // Test with empty value - should return fallback
result := ParsedInt64Value("", 42)
tests := []struct { if result != 42 {
name string t.Errorf("Expected 42, got %d", result)
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,
},
} }
for _, tt := range tests { // Test with valid int64
t.Run(tt.name, func(t *testing.T) { result = ParsedInt64Value("9223372036854775807", 42)
parser := NewParser() if result != 9223372036854775807 {
var err error 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)
} }
// 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 { if err != nil {
t.Fatalf("parseLines() error = %v", err) t.Errorf("Unexpected error: %v", err)
}
if result != fallbackURL {
t.Errorf("Expected %v, got %v", fallbackURL, result)
} }
opts := parser.opts // Test with valid URL
if !reflect.DeepEqual(opts.ListenAddr(), tt.expected) { result, err = parseURLValue("https://example.com", nil)
t.Errorf("ListenAddr() got = %v, want %v", opts.ListenAddr(), tt.expected) 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")
} }
} }

View file

@ -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
}

View file

@ -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)
}
}
})
}
}

View file

@ -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)) slog.Error("ACME HTTP challenge server failed", slog.Any("error", err))
} }
}() }()
config.Opts.HTTPS = true config.Opts.SetHTTPSValue(true)
httpServers = append(httpServers, challengeServer) httpServers = append(httpServers, challengeServer)
} }
@ -95,7 +95,7 @@ func StartWebServer(store *storage.Storage, pool *worker.Pool) []*http.Server {
case certFile != "" && keyFile != "": case certFile != "" && keyFile != "":
server.Addr = listenAddr server.Addr = listenAddr
startTLSServer(server, certFile, keyFile) startTLSServer(server, certFile, keyFile)
config.Opts.HTTPS = true config.Opts.SetHTTPSValue(true)
default: default:
server.Addr = listenAddr server.Addr = listenAddr
startHTTPServer(server) startHTTPServer(server)
@ -148,7 +148,7 @@ func startUnixSocketServer(server *http.Server, socketFile string) {
slog.String("key_file", keyFile), slog.String("key_file", keyFile),
) )
// Ensure HTTPS is marked as true if any listener uses TLS // 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 { if err := server.ServeTLS(listener, certFile, keyFile); err != http.ErrServerClosed {
printErrorAndExit("TLS Unix socket server failed to start on %s: %v", socketFile, err) printErrorAndExit("TLS Unix socket server failed to start on %s: %v", socketFile, err)
} }

View file

@ -20,7 +20,7 @@ func middleware(next http.Handler) http.Handler {
ctx = context.WithValue(ctx, request.ClientIPContextKey, clientIP) ctx = context.WithValue(ctx, request.ClientIPContextKey, clientIP)
if r.Header.Get("X-Forwarded-Proto") == "https" { if r.Header.Get("X-Forwarded-Proto") == "https" {
config.Opts.HTTPS = true config.Opts.SetHTTPSValue(true)
} }
t1 := time.Now() 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") w.Header().Set("Strict-Transport-Security", "max-age=31536000")
} }

View file

@ -19,7 +19,7 @@ func TestProxyFilterWithHttpDefault(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -43,7 +43,7 @@ func TestProxyFilterWithHttpsDefault(t *testing.T) {
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image") os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -66,7 +66,7 @@ func TestProxyFilterWithHttpNever(t *testing.T) {
os.Setenv("MEDIA_PROXY_MODE", "none") os.Setenv("MEDIA_PROXY_MODE", "none")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -89,7 +89,7 @@ func TestProxyFilterWithHttpsNever(t *testing.T) {
os.Setenv("MEDIA_PROXY_MODE", "none") os.Setenv("MEDIA_PROXY_MODE", "none")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -114,7 +114,7 @@ func TestProxyFilterWithHttpAlways(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -139,7 +139,7 @@ func TestProxyFilterWithHttpsAlways(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -164,7 +164,7 @@ func TestAbsoluteProxyFilterWithHttpsAlways(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -188,7 +188,7 @@ func TestAbsoluteProxyFilterWithCustomPortAndSubfolderInBaseURL(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -226,7 +226,7 @@ func TestAbsoluteProxyFilterWithHttpsAlwaysAndAudioTag(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) 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") os.Setenv("MEDIA_PROXY_CUSTOM_URL", "https://proxy-example/proxy")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -276,7 +276,7 @@ func TestProxyFilterWithHttpsAlwaysAndIncorrectCustomProxyServer(t *testing.T) {
os.Setenv("MEDIA_PROXY_CUSTOM_URL", "http://:8080example.com") os.Setenv("MEDIA_PROXY_CUSTOM_URL", "http://:8080example.com")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err == nil { 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()) 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") os.Setenv("MEDIA_PROXY_CUSTOM_URL", "https://proxy-example/proxy")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) 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_MODE", "invalid")
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error if _, err := config.NewConfigParser().ParseEnvironmentVariables(); err == nil {
parser := config.NewParser() t.Fatalf(`Parsing should have failed (MEDIA_PROXY_MODE=%q): %q`, os.Getenv("MEDIA_PROXY_MODE"), config.Opts.MediaProxyMode())
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 := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
output := RewriteDocumentWithRelativeProxyURL(r, input)
expected := `<p><img src="/proxy/okK5PsdNY8F082UMQEAbLPeUFfbe2WnNfInNmR9T4WA=/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
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 := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
output := RewriteDocumentWithRelativeProxyURL(r, input)
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got %q instead of %q`, output, expected)
} }
} }
@ -363,7 +325,7 @@ func TestProxyFilterWithSrcset(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -388,7 +350,7 @@ func TestProxyFilterWithEmptySrcset(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -413,7 +375,7 @@ func TestProxyFilterWithPictureSource(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -433,12 +395,12 @@ func TestProxyFilterWithPictureSource(t *testing.T) {
func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) { func TestProxyFilterOnlyNonHTTPWithPictureSource(t *testing.T) {
os.Clearenv() 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_RESOURCE_TYPES", "image")
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -462,7 +424,7 @@ func TestProxyWithImageDataURL(t *testing.T) {
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image") os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -486,7 +448,7 @@ func TestProxyWithImageSourceDataURL(t *testing.T) {
os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image") os.Setenv("MEDIA_PROXY_RESOURCE_TYPES", "image")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -511,7 +473,7 @@ func TestProxyFilterWithVideo(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -536,7 +498,7 @@ func TestProxyFilterVideoPoster(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -561,7 +523,7 @@ func TestProxyFilterVideoPosterOnce(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)

View file

@ -290,7 +290,7 @@ func TestEnclosure_ProxifyEnclosureURL(t *testing.T) {
os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Config parsing failure: %v`, err) 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") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Config parsing failure: %v`, err) 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") os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Config parsing failure: %v`, err) t.Fatalf(`Config parsing failure: %v`, err)

View file

@ -80,7 +80,7 @@ func TestFeedScheduleNextCheckRoundRobinDefault(t *testing.T) {
os.Clearenv() os.Clearenv()
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -102,7 +102,7 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMinInterval(t *test
os.Clearenv() os.Clearenv()
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -125,7 +125,7 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayBelowMinInterval(t *test
os.Clearenv() os.Clearenv()
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -148,7 +148,7 @@ func TestFeedScheduleNextCheckRoundRobinWithRefreshDelayAboveMaxInterval(t *test
os.Clearenv() os.Clearenv()
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) 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)) os.Setenv("SCHEDULER_ROUND_ROBIN_MIN_INTERVAL", strconv.Itoa(minInterval))
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) 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)) os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval))
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -230,7 +230,7 @@ func TestFeedScheduleNextCheckEntryFrequencyMaxIntervalZeroWeeklyCount(t *testin
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval)) os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval))
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) 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)) os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval))
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
@ -286,7 +286,7 @@ func TestFeedScheduleNextCheckEntryFrequencyFactor(t *testing.T) {
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_FACTOR", strconv.Itoa(factor)) os.Setenv("SCHEDULER_ENTRY_FREQUENCY_FACTOR", strconv.Itoa(factor))
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) 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)) os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval))
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) 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)) os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", strconv.Itoa(minInterval))
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)

View file

@ -414,7 +414,7 @@ func TestKeeplistRulesBehavior(t *testing.T) {
// Tests for isBlockedGlobally function // Tests for isBlockedGlobally function
func TestIsBlockedGlobally(t *testing.T) { func TestIsBlockedGlobally(t *testing.T) {
var err error var err error
config.Opts, err = config.NewParser().ParseEnvironmentVariables() config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
} }
@ -429,7 +429,7 @@ func TestIsBlockedGlobally(t *testing.T) {
os.Setenv("FILTER_ENTRY_MAX_AGE_DAYS", "30") os.Setenv("FILTER_ENTRY_MAX_AGE_DAYS", "30")
defer os.Clearenv() defer os.Clearenv()
config.Opts, err = config.NewParser().ParseEnvironmentVariables() config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
} }

View file

@ -31,11 +31,11 @@ func getVideoIDFromYouTubeURL(websiteURL string) string {
} }
func shouldFetchYouTubeWatchTimeForSingleEntry(entry *model.Entry) bool { 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 { func shouldFetchYouTubeWatchTimeInBulk() bool {
return config.Opts.FetchYouTubeWatchTime() && config.Opts.YouTubeApiKey() != "" return config.Opts.FetchYouTubeWatchTime() && config.Opts.YouTubeAPIKey() != ""
} }
func fetchYouTubeWatchTimeForSingleEntry(websiteURL string) (int, error) { func fetchYouTubeWatchTimeForSingleEntry(websiteURL string) (int, error) {
@ -82,7 +82,7 @@ func fetchYouTubeWatchTimeFromApiInBulk(videoIDs []string) (map[string]time.Dura
apiQuery := url.Values{} apiQuery := url.Values{}
apiQuery.Set("id", strings.Join(videoIDs, ",")) apiQuery.Set("id", strings.Join(videoIDs, ","))
apiQuery.Set("key", config.Opts.YouTubeApiKey()) apiQuery.Set("key", config.Opts.YouTubeAPIKey())
apiQuery.Set("part", "contentDetails") apiQuery.Set("part", "contentDetails")
apiURL := url.URL{ apiURL := url.URL{

View file

@ -67,7 +67,7 @@ func TestRewriteWithNoMatchingRule(t *testing.T) {
} }
func TestRewriteYoutubeVideoLink(t *testing.T) { func TestRewriteYoutubeVideoLink(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
controlEntry := &model.Entry{ controlEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234", URL: "https://www.youtube.com/watch?v=1234",
@ -87,7 +87,7 @@ func TestRewriteYoutubeVideoLink(t *testing.T) {
} }
func TestRewriteYoutubeShortLink(t *testing.T) { func TestRewriteYoutubeShortLink(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
controlEntry := &model.Entry{ controlEntry := &model.Entry{
URL: "https://www.youtube.com/shorts/1LUWKWZkPjo", URL: "https://www.youtube.com/shorts/1LUWKWZkPjo",
@ -107,7 +107,7 @@ func TestRewriteYoutubeShortLink(t *testing.T) {
} }
func TestRewriteIncorrectYoutubeLink(t *testing.T) { func TestRewriteIncorrectYoutubeLink(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
controlEntry := &model.Entry{ controlEntry := &model.Entry{
URL: "https://www.youtube.com/some-page", 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/") os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {
@ -156,7 +156,7 @@ func TestRewriteYoutubeLinkAndCustomEmbedURL(t *testing.T) {
} }
func TestRewriteYoutubeVideoLinkUsingInvidious(t *testing.T) { func TestRewriteYoutubeVideoLinkUsingInvidious(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
controlEntry := &model.Entry{ controlEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234", URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`, Title: `A title`,
@ -176,7 +176,7 @@ func TestRewriteYoutubeVideoLinkUsingInvidious(t *testing.T) {
} }
func TestRewriteYoutubeShortLinkUsingInvidious(t *testing.T) { func TestRewriteYoutubeShortLinkUsingInvidious(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
controlEntry := &model.Entry{ controlEntry := &model.Entry{
URL: "https://www.youtube.com/shorts/1LUWKWZkPjo", URL: "https://www.youtube.com/shorts/1LUWKWZkPjo",
Title: `A title`, Title: `A title`,
@ -196,7 +196,7 @@ func TestRewriteYoutubeShortLinkUsingInvidious(t *testing.T) {
} }
func TestAddYoutubeVideoFromId(t *testing.T) { func TestAddYoutubeVideoFromId(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
scenarios := map[string]string{ scenarios := map[string]string{
// Test with single YouTube ID // Test with single YouTube ID
@ -239,7 +239,7 @@ func TestAddYoutubeVideoFromIdWithCustomEmbedURL(t *testing.T) {
os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/")
var err error var err error
parser := config.NewParser() parser := config.NewConfigParser()
config.Opts, err = parser.ParseEnvironmentVariables() config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil { if err != nil {

View file

@ -392,7 +392,7 @@ func TestInvalidNestedTag(t *testing.T) {
} }
func TestInvalidIFrame(t *testing.T) { func TestInvalidIFrame(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
input := `<iframe src="http://example.org/"></iframe>` input := `<iframe src="http://example.org/"></iframe>`
expected := `` expected := ``
@ -404,7 +404,7 @@ func TestInvalidIFrame(t *testing.T) {
} }
func TestSameDomainIFrame(t *testing.T) { func TestSameDomainIFrame(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
input := `<iframe src="http://example.com/test"></iframe>` input := `<iframe src="http://example.com/test"></iframe>`
expected := `` expected := ``
@ -416,7 +416,7 @@ func TestSameDomainIFrame(t *testing.T) {
} }
func TestInvidiousIFrame(t *testing.T) { func TestInvidiousIFrame(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
input := `<iframe src="https://yewtu.be/watch?v=video_id"></iframe>` input := `<iframe src="https://yewtu.be/watch?v=video_id"></iframe>`
expected := `<iframe src="https://yewtu.be/watch?v=video_id" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>` expected := `<iframe src="https://yewtu.be/watch?v=video_id" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
@ -432,7 +432,7 @@ func TestCustomYoutubeEmbedURL(t *testing.T) {
defer os.Clearenv() defer os.Clearenv()
var err error 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) t.Fatalf(`Parsing failure: %v`, err)
} }
@ -446,7 +446,7 @@ func TestCustomYoutubeEmbedURL(t *testing.T) {
} }
func TestIFrameWithChildElements(t *testing.T) { func TestIFrameWithChildElements(t *testing.T) {
config.Opts = config.NewOptions() config.Opts = config.NewConfigOptions()
input := `<iframe src="https://www.youtube.com/"><p>test</p></iframe>` input := `<iframe src="https://www.youtube.com/"><p>test</p></iframe>`
expected := `<iframe src="https://www.youtube.com/" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>` expected := `<iframe src="https://www.youtube.com/" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" loading="lazy"></iframe>`
@ -850,7 +850,7 @@ func TestReplaceYoutubeURLWithCustomURL(t *testing.T) {
os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/") os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/")
var err error var err error
config.Opts, err = config.NewParser().ParseEnvironmentVariables() config.Opts, err = config.NewConfigParser().ParseEnvironmentVariables()
if err != nil { if err != nil {
t.Fatalf(`Parsing failure: %v`, err) t.Fatalf(`Parsing failure: %v`, err)
} }

View file

@ -41,7 +41,7 @@ func (f *funcMap) Map() template.FuncMap {
"baseURL": config.Opts.BaseURL, "baseURL": config.Opts.BaseURL,
"rootURL": config.Opts.RootURL, "rootURL": config.Opts.RootURL,
"disableLocalAuth": config.Opts.DisableLocalAuth, "disableLocalAuth": config.Opts.DisableLocalAuth,
"oidcProviderName": config.Opts.OIDCProviderName, "oidcProviderName": config.Opts.OAuth2OIDCProviderName,
"hasOAuth2Provider": func(provider string) bool { "hasOAuth2Provider": func(provider string) bool {
return config.Opts.OAuth2Provider() == provider return config.Opts.OAuth2Provider() == provider
}, },

View file

@ -33,7 +33,7 @@ func (h *handler) showAboutPage(w http.ResponseWriter, r *http.Request) {
view.Set("user", user) view.Set("user", user)
view.Set("countUnread", h.store.CountUnreadEntries(user.ID)) view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(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("postgres_version", h.store.DatabaseVersion())
view.Set("go_version", runtime.Version()) view.Set("go_version", runtime.Version())

View file

@ -89,7 +89,7 @@ func (h *handler) checkLogin(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, cookie.New( http.SetCookie(w, cookie.New(
cookie.CookieUserSessionID, cookie.CookieUserSessionID,
sessionToken, sessionToken,
config.Opts.HTTPS, config.Opts.HTTPS(),
config.Opts.BasePath(), config.Opts.BasePath(),
)) ))

View file

@ -32,7 +32,7 @@ func (h *handler) logout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, cookie.Expired( http.SetCookie(w, cookie.Expired(
cookie.CookieUserSessionID, cookie.CookieUserSessionID,
config.Opts.HTTPS, config.Opts.HTTPS(),
config.Opts.BasePath(), config.Opts.BasePath(),
)) ))

View file

@ -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 { if r.Method == http.MethodPost {
@ -261,7 +261,7 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
http.SetCookie(w, cookie.New( http.SetCookie(w, cookie.New(
cookie.CookieUserSessionID, cookie.CookieUserSessionID,
sessionToken, sessionToken,
config.Opts.HTTPS, config.Opts.HTTPS(),
config.Opts.BasePath(), config.Opts.BasePath(),
)) ))

View file

@ -16,6 +16,6 @@ func getOAuth2Manager(ctx context.Context) *oauth2.Manager {
config.Opts.OAuth2ClientID(), config.Opts.OAuth2ClientID(),
config.Opts.OAuth2ClientSecret(), config.Opts.OAuth2ClientSecret(),
config.Opts.OAuth2RedirectURL(), config.Opts.OAuth2RedirectURL(),
config.Opts.OIDCDiscoveryEndpoint(), config.Opts.OAuth2OIDCDiscoveryEndpoint(),
) )
} }

View file

@ -145,7 +145,7 @@ func (h *handler) oauth2Callback(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, cookie.New( http.SetCookie(w, cookie.New(
cookie.CookieUserSessionID, cookie.CookieUserSessionID,
sessionToken, sessionToken,
config.Opts.HTTPS, config.Opts.HTTPS(),
config.Opts.BasePath(), config.Opts.BasePath(),
)) ))

View file

@ -331,7 +331,7 @@ func (h *handler) finishLogin(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, cookie.New( http.SetCookie(w, cookie.New(
cookie.CookieUserSessionID, cookie.CookieUserSessionID,
sessionToken, sessionToken,
config.Opts.HTTPS, config.Opts.HTTPS(),
config.Opts.BasePath(), config.Opts.BasePath(),
)) ))

View file

@ -329,7 +329,7 @@ Default is 300 seconds\&.
.B HTTPS .B HTTPS
Forces cookies to use secure flag and send HSTS header\&. Forces cookies to use secure flag and send HSTS header\&.
.br .br
Default is empty\&. Default is disabled\&.
.TP .TP
.B INVIDIOUS_INSTANCE .B INVIDIOUS_INSTANCE
Set a custom invidious instance to use\&. Set a custom invidious instance to use\&.
@ -466,7 +466,7 @@ Default is empty\&.
.B OAUTH2_OIDC_PROVIDER_NAME .B OAUTH2_OIDC_PROVIDER_NAME
Name to display for the OIDC provider\&. Name to display for the OIDC provider\&.
.br .br
Default is OpenID Connect\&. Default is "OpenID Connect"\&.
.TP .TP
.B OAUTH2_PROVIDER .B OAUTH2_PROVIDER
Possible values are "google" or "oidc"\&. Possible values are "google" or "oidc"\&.
@ -537,7 +537,7 @@ Default is 1\&.
.B SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL .B SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL
Maximum interval in minutes for the entry frequency scheduler\&. Maximum interval in minutes for the entry frequency scheduler\&.
.br .br
Default is 24 hours\&. Default is 1440 minutes (24 hours)\&.
.TP .TP
.B SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL .B SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL
Minimum interval in minutes for the entry frequency scheduler\&. Minimum interval in minutes for the entry frequency scheduler\&.