diff --git a/README.md b/README.md index bb5315b..ecbf051 100644 --- a/README.md +++ b/README.md @@ -55,13 +55,23 @@ you can place variables in a `.env` file in the project root (loaded automatically via `godotenv/autoload`). 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 | | ----------------------- | ----------------------------------- | -------- | | `WEBHOOKER_ENVIRONMENT` | `dev` or `prod` | `dev` | | `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` | | `METRICS_USERNAME` | Basic auth username for `/metrics` | `""` | | `METRICS_PASSWORD` | Basic auth password for `/metrics` | `""` | diff --git a/internal/config/config.go b/internal/config/config.go index c21b8ca..b823099 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "os" + "path/filepath" "strconv" "strings" @@ -79,6 +80,23 @@ func envInt(key string, defaultValue int) int { 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 func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) { 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 // (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.IsProd() { s.DataDir = "/data" } else { - s.DataDir = "./data" + s.DataDir = devDataDir() } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f936344..ef141f1 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "os" + "path/filepath" "testing" "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) +}