fix: use absolute path for dev DATA_DIR default, clarify env docs

Change the dev-mode DATA_DIR default from the relative path ./data to
$XDG_DATA_HOME/webhooker (falling back to $HOME/.local/share/webhooker).
This ensures the application's data directory does not depend on the
working directory.

Add a table to the README that clearly documents what WEBHOOKER_ENVIRONMENT
actually controls: DATA_DIR default, CORS policy, and session cookie
Secure flag.

Add tests for devDataDir() and verify the dev default is always absolute.
This commit is contained in:
clawbot
2026-03-17 03:30:30 -07:00
committed by user
parent 8d702a16c6
commit 89af414037
3 changed files with 85 additions and 3 deletions

View File

@@ -55,13 +55,23 @@ you can place variables in a `.env` file in the project root (loaded
automatically via `godotenv/autoload`). automatically via `godotenv/autoload`).
The environment is selected by setting `WEBHOOKER_ENVIRONMENT` to `dev` The environment is selected by setting `WEBHOOKER_ENVIRONMENT` to `dev`
or `prod` (default: `dev`). or `prod` (default: `dev`). The setting controls several behaviors:
| Behavior | `dev` | `prod` |
| --------------------- | -------------------------------- | ------------------------------- |
| Default `DATA_DIR` | `$XDG_DATA_HOME/webhooker` (or `$HOME/.local/share/webhooker`) | `/data` |
| CORS | Allows any origin (`*`) | Disabled (no-op) |
| Session cookie Secure | `false` (works over plain HTTP) | `true` (requires HTTPS) |
All other differences (log format, security headers, etc.) are
independent of the environment setting — log format is determined by
TTY detection, and security headers are always applied.
| Variable | Description | Default | | Variable | Description | Default |
| ----------------------- | ----------------------------------- | -------- | | ----------------------- | ----------------------------------- | -------- |
| `WEBHOOKER_ENVIRONMENT` | `dev` or `prod` | `dev` | | `WEBHOOKER_ENVIRONMENT` | `dev` or `prod` | `dev` |
| `PORT` | HTTP listen port | `8080` | | `PORT` | HTTP listen port | `8080` |
| `DATA_DIR` | Directory for all SQLite databases | `./data` (dev) / `/data` (prod) | | `DATA_DIR` | Directory for all SQLite databases | `$XDG_DATA_HOME/webhooker` (dev) / `/data` (prod) |
| `DEBUG` | Enable debug logging | `false` | | `DEBUG` | Enable debug logging | `false` |
| `METRICS_USERNAME` | Basic auth username for `/metrics` | `""` | | `METRICS_USERNAME` | Basic auth username for `/metrics` | `""` |
| `METRICS_PASSWORD` | Basic auth password for `/metrics` | `""` | | `METRICS_PASSWORD` | Basic auth password for `/metrics` | `""` |

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -79,6 +80,23 @@ func envInt(key string, defaultValue int) int {
return defaultValue return defaultValue
} }
// devDataDir returns the default data directory for the dev
// environment. It uses $XDG_DATA_HOME/webhooker if set, otherwise
// falls back to $HOME/.local/share/webhooker. The result is always
// an absolute path so the application's behavior does not depend on
// the working directory.
func devDataDir() string {
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
return filepath.Join(xdg, "webhooker")
}
home, err := os.UserHomeDir()
if err != nil {
// Last resort: use /tmp so we still have an absolute path.
return filepath.Join(os.TempDir(), "webhooker")
}
return filepath.Join(home, ".local", "share", "webhooker")
}
// nolint:revive // lc parameter is required by fx even if unused // nolint:revive // lc parameter is required by fx even if unused
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) { func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
log := params.Logger.Get() log := params.Logger.Get()
@@ -111,11 +129,13 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
// Set default DataDir based on environment. All SQLite databases // Set default DataDir based on environment. All SQLite databases
// (main application DB and per-webhook event DBs) live here. // (main application DB and per-webhook event DBs) live here.
// Both defaults are absolute paths to avoid dependence on the
// working directory.
if s.DataDir == "" { if s.DataDir == "" {
if s.IsProd() { if s.IsProd() {
s.DataDir = "/data" s.DataDir = "/data"
} else { } else {
s.DataDir = "./data" s.DataDir = devDataDir()
} }
} }

View File

@@ -2,6 +2,7 @@ package config
import ( import (
"os" "os"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -104,3 +105,54 @@ func TestEnvironmentConfig(t *testing.T) {
}) })
} }
} }
func TestDevDataDir(t *testing.T) {
t.Run("uses XDG_DATA_HOME when set", func(t *testing.T) {
os.Setenv("XDG_DATA_HOME", "/custom/data")
defer os.Unsetenv("XDG_DATA_HOME")
got := devDataDir()
assert.Equal(t, "/custom/data/webhooker", got)
})
t.Run("falls back to HOME/.local/share/webhooker", func(t *testing.T) {
os.Unsetenv("XDG_DATA_HOME")
home, err := os.UserHomeDir()
require.NoError(t, err)
got := devDataDir()
assert.Equal(t, filepath.Join(home, ".local", "share", "webhooker"), got)
})
t.Run("result is always absolute", func(t *testing.T) {
os.Unsetenv("XDG_DATA_HOME")
got := devDataDir()
assert.True(t, filepath.IsAbs(got), "devDataDir() returned relative path: %s", got)
})
}
func TestDevDefaultDataDirIsAbsolute(t *testing.T) {
// Verify that when WEBHOOKER_ENVIRONMENT=dev and DATA_DIR is unset,
// the resulting DataDir is an absolute path.
os.Unsetenv("WEBHOOKER_ENVIRONMENT")
os.Unsetenv("DATA_DIR")
var cfg *Config
app := fxtest.New(
t,
fx.Provide(
globals.New,
logger.New,
New,
),
fx.Populate(&cfg),
)
require.NoError(t, app.Err())
app.RequireStart()
defer app.RequireStop()
assert.True(t, filepath.IsAbs(cfg.DataDir),
"dev default DataDir should be absolute, got: %s", cfg.DataDir)
}