mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-19 12:46:39 +08:00
fix(config): 完成生产配置向 config.toml 的迁移
This commit is contained in:
@@ -332,7 +332,7 @@ func TestRefreshCurrentReleaseHealthTransitionsAndClearsFailure(t *testing.T) {
|
||||
t.Fatalf("refresh diagnostics=%v", refresh)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("1234567890"), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(root, "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"), []byte("2801109426\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backend.releaseStatusErr = nil
|
||||
@@ -461,7 +461,7 @@ func TestCDNSupportsRangeHeadAndConditionalRequests(t *testing.T) {
|
||||
t.Fatalf("Content-Range=%q", rr.Header().Get("Content-Range"))
|
||||
}
|
||||
etag := rr.Header().Get("ETag")
|
||||
if etag != `"blake3-0000000000000000000000000000000000000000000000000000000000000000"` {
|
||||
if etag != `"blake3-5d5c6cc8ca0afa7d71df9b0d764c7b7f9e60c8b082dbb9fc844b10442989357f"` {
|
||||
t.Fatalf("ETag=%q", etag)
|
||||
}
|
||||
|
||||
@@ -1802,21 +1802,6 @@ func TestDiscoverTreatsCatalogUnavailableAsNoRelease(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadEnvFileDoesNotOverride(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, ".env")
|
||||
if err := os.WriteFile(path, []byte("BAT_API_LISTEN=:9999\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("BAT_API_LISTEN", ":1111")
|
||||
if err := LoadEnvFile(path); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if os.Getenv("BAT_API_LISTEN") != ":1111" {
|
||||
t.Fatal(os.Getenv("BAT_API_LISTEN"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshDiagnosticsAndReadyz(t *testing.T) {
|
||||
root := fixtureRoot(t)
|
||||
cfg := DefaultConfig()
|
||||
|
||||
+6
-102
@@ -25,14 +25,16 @@ const (
|
||||
DefaultAuthSkew = 5 * time.Minute
|
||||
ClientPatchHost = "prod-clientpatch.bluearchiveyostar.com"
|
||||
ServerInfoHost = "yostar-serverinfo.bluearchiveyostar.com"
|
||||
EnvFileName = ".env"
|
||||
ConfigFileName = "config.toml"
|
||||
ConfigExampleName = "config.toml.example"
|
||||
)
|
||||
|
||||
// Config holds bat-api process configuration.
|
||||
//
|
||||
// Values come from (highest wins): CLI flags > process environment > .env file
|
||||
// > built-in defaults. Database-related keys are reserved for future API
|
||||
// persistence and are loaded but not required for the resource CDN surface.
|
||||
// Values come from (highest wins): CLI flags > process environment >
|
||||
// config.toml > built-in defaults. Database-related keys are reserved for
|
||||
// future API persistence and are loaded but not required for the resource CDN
|
||||
// surface.
|
||||
type Config struct {
|
||||
// Listen is the HTTP listen address, e.g. ":18080" or "127.0.0.1:18080".
|
||||
Listen string
|
||||
@@ -54,8 +56,6 @@ type Config struct {
|
||||
RPCTimeout time.Duration
|
||||
// RefreshInterval periodically re-discovers the release through RPC. Zero disables it.
|
||||
RefreshInterval time.Duration
|
||||
// SkipEnvFile disables loading .env when true (BAT_API_SKIP_ENV_FILE=1).
|
||||
SkipEnvFile bool
|
||||
// AuthToken enables HTTP token authentication when non-empty.
|
||||
AuthToken string
|
||||
// AuthQueryParam is the optional query parameter accepted for token auth.
|
||||
@@ -148,41 +148,6 @@ func (c *Config) Normalize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadEnvFile reads KEY=VALUE lines from path into the process environment
|
||||
// without overriding variables that are already set.
|
||||
func LoadEnvFile(path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.Trim(value, `"'`)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := os.LookupEnv(key); exists {
|
||||
continue
|
||||
}
|
||||
if err := os.Setenv(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyEnv overlays environment variables onto cfg.
|
||||
func ApplyEnv(cfg *Config) {
|
||||
if v := os.Getenv("BAT_API_LISTEN"); v != "" {
|
||||
@@ -239,9 +204,6 @@ func ApplyEnv(cfg *Config) {
|
||||
if v := os.Getenv("BAT_API_REDIS_PASSWORD"); v != "" {
|
||||
cfg.RedisPassword = v
|
||||
}
|
||||
if v := os.Getenv("BAT_API_SKIP_ENV_FILE"); v != "" {
|
||||
cfg.SkipEnvFile = parseBool(v, false)
|
||||
}
|
||||
if v := os.Getenv("BAT_API_AUTH_TOKEN"); v != "" {
|
||||
cfg.AuthToken = v
|
||||
}
|
||||
@@ -313,61 +275,3 @@ func normalizePathList(paths []string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// EnvTemplate is written next to the bat-api binary on first run when missing.
|
||||
const EnvTemplate = `# bat-api configuration
|
||||
# Priority: CLI flags > process environment > this file > built-in defaults.
|
||||
# Resource auto-pull/sync is owned by the Rust bat daemon, not bat-api.
|
||||
# bat-api only bootstraps/distributes already-published resources and exposes
|
||||
# a small management surface for release inspection.
|
||||
|
||||
# HTTP listen address (host:port or :port)
|
||||
BAT_API_LISTEN=:18080
|
||||
# Public base URL used when rewriting AddressablesCatalogUrlRoot
|
||||
BAT_API_PUBLIC_BASE_URL=http://127.0.0.1:18080
|
||||
|
||||
# Rust bat daemon RPC socket (primary discovery path)
|
||||
# Prefer BAT_API_SOCKET; BAT_API_STATE_DIR only derives the default socket path.
|
||||
BAT_API_STATE_DIR=/tmp/bat-pid
|
||||
# BAT_API_SOCKET=/tmp/bat-pid/bat.sock
|
||||
|
||||
# Optional override of the published release root (fixtures / emergency only).
|
||||
# Production obtains resource_root from daemon RPC (release.attestation +
|
||||
# generation-bound resource.manifest).
|
||||
# BAT_API_RESOURCE_ROOT=
|
||||
|
||||
# Optional server-info JSON for Addressables root rewrite
|
||||
# BAT_API_SERVER_INFO_FILE=
|
||||
|
||||
# CDN safety
|
||||
BAT_API_REQUIRE_INDEXED=true
|
||||
BAT_API_VERIFY_SIZE=true
|
||||
BAT_API_RPC_TIMEOUT=30s
|
||||
# Periodically re-read bat.sock so bat-api follows Rust bat release switches.
|
||||
# Set to 0 to disable in fixture-only local development.
|
||||
BAT_API_REFRESH_INTERVAL=1m
|
||||
|
||||
# Player-facing HTTP controls.
|
||||
# Prefer setting BAT_API_AUTH_TOKEN through a secret manager or process
|
||||
# environment. Reverse proxies may inject Authorization: Bearer <token> or
|
||||
# X-BAT-Token to authenticated upstream requests. Query token fallback uses
|
||||
# BAT_API_AUTH_QUERY_PARAM and is supported for clients that cannot set headers.
|
||||
# BAT_API_AUTH_TOKEN=
|
||||
BAT_API_AUTH_QUERY_PARAM=bat_token
|
||||
# BAT_API_AUTH_EXEMPT_PATHS=
|
||||
BAT_API_TRUST_PROXY_HEADERS=false
|
||||
BAT_API_ACCESS_LOG=false
|
||||
# Zero disables in-process limiting; production may still enforce edge limits.
|
||||
BAT_API_RATE_LIMIT_RPS=0
|
||||
BAT_API_RATE_LIMIT_BURST=0
|
||||
BAT_API_MAX_RESOURCE_LIMIT=1000
|
||||
|
||||
# Reserved for future API persistence (not required for resource CDN)
|
||||
# BAT_API_DATABASE_URL=postgres://bat:@127.0.0.1:5432/bat?sslmode=disable
|
||||
# BAT_API_DATABASE_PASSWORD=
|
||||
# BAT_API_REDIS_URL=redis://127.0.0.1:6379/0
|
||||
# BAT_API_REDIS_PASSWORD=
|
||||
|
||||
# Set to 1 to ignore this file entirely
|
||||
# BAT_API_SKIP_ENV_FILE=0
|
||||
`
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// ConfigTOMLTemplate is the only template generated by bat-api. Rust bat
|
||||
// accepts the same file and ignores the [api] section.
|
||||
const ConfigTOMLTemplate = `# BlueArchive Toolkit shared application configuration.
|
||||
# Priority: CLI flags > process environment > config.toml > built-in defaults.
|
||||
# This file is the application configuration. The application never reads .env.
|
||||
#
|
||||
# Rust bat consumes [runtime], [resource], [localized], [repository], [network]
|
||||
# and [translation.worker]. Go bat-api consumes [api]. Each binary ignores the
|
||||
# other application's section.
|
||||
|
||||
[runtime]
|
||||
state_dir = '/tmp/bat-pid'
|
||||
interval_seconds = 3600
|
||||
error_retry_seconds = 60
|
||||
quiet_up_to_date = false
|
||||
output_format = 'human'
|
||||
banner = true
|
||||
progress = true
|
||||
tail_lines = 200
|
||||
|
||||
[resource]
|
||||
output_root = './bat-resources'
|
||||
auto_discover = true
|
||||
app_version = ''
|
||||
connection_group = ''
|
||||
launcher_version = '1.7.2'
|
||||
platforms = ['windows', 'android']
|
||||
snapshot_path = ''
|
||||
dry_run = false
|
||||
plan = false
|
||||
force = false
|
||||
audit_local = true
|
||||
repair = true
|
||||
|
||||
[resource.server_info]
|
||||
kind = 'none'
|
||||
value = ''
|
||||
|
||||
[localized]
|
||||
output_root = './bat-localized'
|
||||
|
||||
[repository]
|
||||
import_repository = false
|
||||
import_cas_root = ''
|
||||
import_resource_repository_path = ''
|
||||
|
||||
[network]
|
||||
curl_command = 'curl'
|
||||
proxy = 'auto'
|
||||
unzip_command = 'unzip'
|
||||
zip_command = 'zip'
|
||||
download_concurrency = 8
|
||||
|
||||
[translation.worker]
|
||||
provider = 'mock'
|
||||
fixture = ''
|
||||
translation_memory_path = ''
|
||||
glossary_path = ''
|
||||
concurrency = 8
|
||||
max_attempts = 3
|
||||
lease_seconds = 300
|
||||
retry_backoff_seconds = 5
|
||||
max_tasks = ''
|
||||
worker_id = ''
|
||||
|
||||
[api]
|
||||
listen = ':18080'
|
||||
public_base_url = 'http://127.0.0.1:18080'
|
||||
state_dir = '/tmp/bat-pid'
|
||||
socket_path = ''
|
||||
resource_root = ''
|
||||
server_info_file = ''
|
||||
require_indexed = true
|
||||
verify_size = true
|
||||
rpc_timeout = '30s'
|
||||
refresh_interval = '1m'
|
||||
auth_token = ''
|
||||
auth_query_param = 'bat_token'
|
||||
auth_exempt_paths = []
|
||||
trust_proxy_headers = false
|
||||
access_log = false
|
||||
rate_limit_rps = 0
|
||||
rate_limit_burst = 0
|
||||
max_resource_page_limit = 1000
|
||||
|
||||
# Reserved for a future API persistence layer.
|
||||
database_url = ''
|
||||
database_password = ''
|
||||
redis_url = ''
|
||||
redis_password = ''
|
||||
`
|
||||
|
||||
// LoadConfigFromCurrentExe loads config.toml next to the running binary.
|
||||
func LoadConfigFromCurrentExe(cfg *Config) error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate current executable: %w", err)
|
||||
}
|
||||
return LoadConfigFromBinaryDir(filepath.Dir(exe), cfg)
|
||||
}
|
||||
|
||||
// LoadConfigFromBinaryDir loads only config.toml from binaryDir. Missing
|
||||
// config.toml causes config.toml.example to be created when possible; the
|
||||
// example is never parsed as the active configuration.
|
||||
func LoadConfigFromBinaryDir(binaryDir string, cfg *Config) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config must not be nil")
|
||||
}
|
||||
configPath := filepath.Join(binaryDir, ConfigFileName)
|
||||
examplePath := filepath.Join(binaryDir, ConfigExampleName)
|
||||
info, err := os.Lstat(configPath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("stat %s: %w", configPath, err)
|
||||
}
|
||||
ensureConfigExample(examplePath)
|
||||
return nil
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("%s must be a regular file", configPath)
|
||||
}
|
||||
if runtime.GOOS != "windows" && info.Mode().Perm()&0o077 != 0 {
|
||||
return fmt.Errorf("%s permissions must be 0600 or stricter (current %03o)", configPath, info.Mode().Perm())
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", configPath, err)
|
||||
}
|
||||
if err := applyAPITOML(data, cfg); err != nil {
|
||||
return fmt.Errorf("parse %s: %w", configPath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureConfigExample(path string) {
|
||||
if _, err := os.Lstat(path); err == nil || !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err := file.WriteString(ConfigTOMLTemplate); err != nil {
|
||||
_ = file.Close()
|
||||
return
|
||||
}
|
||||
_ = file.Close()
|
||||
}
|
||||
|
||||
func applyAPITOML(data []byte, cfg *Config) error {
|
||||
var document map[string]any
|
||||
if err := toml.Unmarshal(data, &document); err != nil {
|
||||
return err
|
||||
}
|
||||
rawAPI, exists := document["api"]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
apiValues, ok := rawAPI.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("[api] must be a table")
|
||||
}
|
||||
for key, value := range apiValues {
|
||||
if err := applyAPIValue(cfg, key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyAPIValue(cfg *Config, key string, raw any) error {
|
||||
field := fmt.Sprintf("[api].%s", key)
|
||||
switch key {
|
||||
case "listen":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.Listen = value
|
||||
case "public_base_url":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.PublicBaseURL = value
|
||||
case "state_dir":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.StateDir = value
|
||||
case "socket_path":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.SocketPath = value
|
||||
case "resource_root":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.ResourceRoot = value
|
||||
case "server_info_file":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.ServerInfoFile = value
|
||||
case "require_indexed":
|
||||
value, err := tomlBool(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RequireIndexed = value
|
||||
case "verify_size":
|
||||
value, err := tomlBool(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.VerifySize = value
|
||||
case "rpc_timeout":
|
||||
value, err := tomlDuration(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RPCTimeout = value
|
||||
case "refresh_interval":
|
||||
value, err := tomlDuration(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RefreshInterval = value
|
||||
case "auth_token":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.AuthToken = value
|
||||
case "auth_query_param":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.AuthQueryParam = value
|
||||
case "auth_exempt_paths":
|
||||
value, err := tomlStringArray(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.AuthExemptPaths = value
|
||||
case "trust_proxy_headers":
|
||||
value, err := tomlBool(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.TrustProxyHeaders = value
|
||||
case "access_log":
|
||||
value, err := tomlBool(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.AccessLog = value
|
||||
case "rate_limit_rps":
|
||||
value, err := tomlNonNegativeNumber(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RateLimitRPS = value
|
||||
case "rate_limit_burst":
|
||||
value, err := tomlNonNegativeInt(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RateLimitBurst = value
|
||||
case "max_resource_page_limit":
|
||||
value, err := tomlNonNegativeInt(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.MaxResourcePageLimit = value
|
||||
case "database_url":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.DatabaseURL = value
|
||||
case "database_password":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.DatabasePassword = value
|
||||
case "redis_url":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RedisURL = value
|
||||
case "redis_password":
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.RedisPassword = value
|
||||
default:
|
||||
return fmt.Errorf("unsupported [api] key %q", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tomlString(raw any, field string) (string, error) {
|
||||
value, ok := raw.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%s must be a string (got %T)", field, raw)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func tomlBool(raw any, field string) (bool, error) {
|
||||
value, ok := raw.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("%s must be a boolean (got %T)", field, raw)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func tomlDuration(raw any, field string) (time.Duration, error) {
|
||||
value, err := tomlString(raw, field)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
duration, err := time.ParseDuration(value)
|
||||
if err != nil || duration < 0 {
|
||||
return 0, fmt.Errorf("%s must be a non-negative duration", field)
|
||||
}
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
func tomlNonNegativeNumber(raw any, field string) (float64, error) {
|
||||
var value float64
|
||||
switch typed := raw.(type) {
|
||||
case int64:
|
||||
value = float64(typed)
|
||||
case float64:
|
||||
value = typed
|
||||
default:
|
||||
return 0, fmt.Errorf("%s must be a non-negative number (got %T)", field, raw)
|
||||
}
|
||||
if value < 0 || math.IsNaN(value) || math.IsInf(value, 0) {
|
||||
return 0, fmt.Errorf("%s must be a non-negative number", field)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func tomlNonNegativeInt(raw any, field string) (int, error) {
|
||||
value, ok := raw.(int64)
|
||||
if !ok || value < 0 || int64(int(value)) != value {
|
||||
return 0, fmt.Errorf("%s must be a non-negative integer (got %T)", field, raw)
|
||||
}
|
||||
return int(value), nil
|
||||
}
|
||||
|
||||
func tomlStringArray(raw any, field string) ([]string, error) {
|
||||
values, ok := raw.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s must be an array of strings (got %T)", field, raw)
|
||||
}
|
||||
items := make([]string, len(values))
|
||||
for index, rawItem := range values {
|
||||
item, ok := rawItem.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s[%d] must be a string (got %T)", field, index, rawItem)
|
||||
}
|
||||
items[index] = item
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func writePrivateConfig(t *testing.T, dir, content string) {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, ConfigFileName)
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFromBinaryDirFirstLaunchCreatesExampleOnly(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if err := LoadConfigFromBinaryDir(dir, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, ConfigExampleName)); err != nil {
|
||||
t.Fatalf("config.toml.example was not created: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, ".env")); !os.IsNotExist(err) {
|
||||
t.Fatalf("unexpected .env after first launch, err=%v", err)
|
||||
}
|
||||
if cfg.Listen != DefaultListen || cfg.RPCTimeout != 30*time.Second {
|
||||
t.Fatalf("first launch changed defaults: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckedInBatAPIConfigExampleMatchesRuntimeTemplate(t *testing.T) {
|
||||
_, sourceFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
path := filepath.Join(filepath.Dir(sourceFile), "..", "..", "cmd", "bat-api", ConfigExampleName)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != ConfigTOMLTemplate {
|
||||
t.Fatal("cmd/bat-api/config.toml.example differs from runtime template")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFromBinaryDirDoesNotReadDotEnv(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte("BAT_API_LISTEN=':19999'\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if err := LoadConfigFromBinaryDir(dir, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Listen != DefaultListen {
|
||||
t.Fatalf("dotenv value was applied: %q", cfg.Listen)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, ConfigExampleName)); err != nil {
|
||||
t.Fatalf("config.toml.example was not created: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFromBinaryDirAppliesAPISectionOnly(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePrivateConfig(t, dir, `
|
||||
[runtime]
|
||||
state_dir = '/rust-state'
|
||||
this_key_belongs_to_rust = 'ignored-by-go'
|
||||
|
||||
[translation.worker]
|
||||
provider = 'mock'
|
||||
|
||||
[api]
|
||||
listen = '127.0.0.1:19080'
|
||||
public_base_url = 'https://assets.example.test'
|
||||
state_dir = '/api-state'
|
||||
socket_path = '/api-state/bat.sock'
|
||||
resource_root = '/srv/releases/current'
|
||||
server_info_file = '/srv/server-info.json'
|
||||
require_indexed = false
|
||||
verify_size = false
|
||||
rpc_timeout = '2s'
|
||||
refresh_interval = '0s'
|
||||
auth_token = 'secret'
|
||||
auth_query_param = 'token'
|
||||
auth_exempt_paths = ['/healthz', '/readyz']
|
||||
trust_proxy_headers = true
|
||||
access_log = true
|
||||
rate_limit_rps = 12.5
|
||||
rate_limit_burst = 30
|
||||
max_resource_page_limit = 250
|
||||
database_url = 'postgres://example'
|
||||
database_password = 'db-secret'
|
||||
redis_url = 'redis://example'
|
||||
redis_password = 'redis-secret'
|
||||
`)
|
||||
cfg := DefaultConfig()
|
||||
|
||||
if err := LoadConfigFromBinaryDir(dir, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Listen != "127.0.0.1:19080" ||
|
||||
cfg.PublicBaseURL != "https://assets.example.test" ||
|
||||
cfg.StateDir != "/api-state" ||
|
||||
cfg.SocketPath != "/api-state/bat.sock" ||
|
||||
cfg.ResourceRoot != "/srv/releases/current" ||
|
||||
cfg.ServerInfoFile != "/srv/server-info.json" ||
|
||||
cfg.RequireIndexed ||
|
||||
cfg.VerifySize ||
|
||||
cfg.RPCTimeout != 2*time.Second ||
|
||||
cfg.RefreshInterval != 0 ||
|
||||
cfg.AuthToken != "secret" ||
|
||||
cfg.AuthQueryParam != "token" ||
|
||||
cfg.TrustProxyHeaders != true ||
|
||||
cfg.AccessLog != true ||
|
||||
cfg.RateLimitRPS != 12.5 ||
|
||||
cfg.RateLimitBurst != 30 ||
|
||||
cfg.MaxResourcePageLimit != 250 ||
|
||||
cfg.DatabaseURL != "postgres://example" ||
|
||||
cfg.DatabasePassword != "db-secret" ||
|
||||
cfg.RedisURL != "redis://example" ||
|
||||
cfg.RedisPassword != "redis-secret" {
|
||||
t.Fatalf("unexpected config: %+v", cfg)
|
||||
}
|
||||
if strings.Join(cfg.AuthExemptPaths, ",") != "/healthz,/readyz" {
|
||||
t.Fatalf("auth exempt paths=%v", cfg.AuthExemptPaths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigFromBinaryDirRejectsUnknownAPIKey(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePrivateConfig(t, dir, "[api]\nunknown_key = 'value'\n")
|
||||
cfg := DefaultConfig()
|
||||
|
||||
err := LoadConfigFromBinaryDir(dir, &cfg)
|
||||
if err == nil || !strings.Contains(err.Error(), "unsupported [api] key") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPrecedenceEnvironmentOverConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePrivateConfig(t, dir, "[api]\nlisten = ':19080'\nauth_token = 'config-token'\n")
|
||||
t.Setenv("BAT_API_LISTEN", ":19081")
|
||||
t.Setenv("BAT_API_AUTH_TOKEN", "env-token")
|
||||
|
||||
cfg := DefaultConfig()
|
||||
if err := LoadConfigFromBinaryDir(dir, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ApplyEnv(&cfg)
|
||||
if cfg.Listen != ":19081" || cfg.AuthToken != "env-token" {
|
||||
t.Fatalf("environment did not override config: %+v", cfg)
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,18 @@
|
||||
"url": "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes",
|
||||
"destination": "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes",
|
||||
"bytes": 21,
|
||||
"blake3": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"blake3": "5d5c6cc8ca0afa7d71df9b0d764c7b7f9e60c8b082dbb9fc844b10442989357f"
|
||||
},
|
||||
"https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash": {
|
||||
"url": "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash",
|
||||
"destination": "prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash",
|
||||
"bytes": 10,
|
||||
"blake3": "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"bytes": 11,
|
||||
"blake3": "dd3384e165b93bc64bf856fcf7d13883bb0a26c3d097de6622a8631b3941f6ef"
|
||||
}
|
||||
}
|
||||
},
|
||||
"destination_index": {
|
||||
"prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes": "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes",
|
||||
"prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash": "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.hash"
|
||||
},
|
||||
"distribution_mapping_identity": "odm-v1-8f515f013cbe2ec7116fee902ea44d0179c435812cd29571361b91efc5ac33e6"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1234567890
|
||||
2801109426
|
||||
|
||||
Reference in New Issue
Block a user