package config import ( "strings" "testing" "time" ) // setEnv sets env vars for the test and clears them afterward, so cases don't // leak into one another. t.Setenv handles restoration. func setEnv(t *testing.T, kv map[string]string) { t.Helper() for k, v := range kv { t.Setenv(k, v) } } func TestLoad_AppliesDefaults(t *testing.T) { // Clear the ones with defaults so we observe the fallback, not the host env. setEnv(t, map[string]string{ "TAPIR_GATEWAY_URL": "", "TAPIR_SUMMARIZER_MODEL": "", "TAPIR_YT_TOKEN_REF": "", "TAPIR_OAUTH_REDIRECT_ADDR": "", "TAPIR_SUMMARIZER_TIMEOUT": "", "TAPIR_POLL_INTERVAL": "", }) c, err := Load() if err != nil { t.Fatalf("Load: %v", err) } if c.GatewayURL != defaultGatewayURL { t.Errorf("GatewayURL = %q, want default %q", c.GatewayURL, defaultGatewayURL) } if c.SummarizerModel != defaultSummarizerModel { t.Errorf("SummarizerModel = %q, want default %q", c.SummarizerModel, defaultSummarizerModel) } if c.YTTokenRef != defaultYTTokenRef { t.Errorf("YTTokenRef = %q, want default %q", c.YTTokenRef, defaultYTTokenRef) } if c.SummarizerTimeout != defaultSummarizerTimeout { t.Errorf("SummarizerTimeout = %v, want default %v", c.SummarizerTimeout, defaultSummarizerTimeout) } if c.PollInterval != 0 { t.Errorf("PollInterval = %v, want 0 (run once)", c.PollInterval) } if c.FetchBackoff != defaultFetchBackoff { t.Errorf("FetchBackoff = %v, want default %v", c.FetchBackoff, defaultFetchBackoff) } } func TestLoad_ParsesValues(t *testing.T) { setEnv(t, map[string]string{ "TAPIR_USER_ID": "11111111-1111-1111-1111-111111111111", "TAPIR_GATEWAY_URL": "http://example/v1", "TAPIR_GATEWAY_KEY": "sk-test", "TAPIR_SUMMARIZER_MODEL": "iguana/deepseek-r1-14b", "TAPIR_SUMMARIZER_TIMEOUT": "90s", "TAPIR_DB_DSN": "postgres://x", "TAPIR_POLL_INTERVAL": "10m", "TAPIR_FETCH_BACKOFF": "30m", }) c, err := Load() if err != nil { t.Fatalf("Load: %v", err) } if c.UserID != "11111111-1111-1111-1111-111111111111" { t.Errorf("UserID = %q", c.UserID) } if c.GatewayKey != "sk-test" { t.Errorf("GatewayKey = %q", c.GatewayKey) } if c.SummarizerModel != "iguana/deepseek-r1-14b" { t.Errorf("SummarizerModel = %q", c.SummarizerModel) } if c.SummarizerTimeout != 90*time.Second { t.Errorf("SummarizerTimeout = %v, want 90s", c.SummarizerTimeout) } if c.PollInterval != 10*time.Minute { t.Errorf("PollInterval = %v, want 10m", c.PollInterval) } if c.FetchBackoff != 30*time.Minute { t.Errorf("FetchBackoff = %v, want 30m", c.FetchBackoff) } } func TestLoad_RejectsBadDuration(t *testing.T) { setEnv(t, map[string]string{"TAPIR_SUMMARIZER_TIMEOUT": "not-a-duration"}) if _, err := Load(); err == nil { t.Fatal("want error on unparsable duration, got nil") } } func TestValidateForRun_ReportsMissing(t *testing.T) { c := Config{} // nothing set err := c.ValidateForRun() if err == nil { t.Fatal("want error when required run fields are missing") } for _, want := range []string{"TAPIR_USER_ID", "TAPIR_DB_DSN", "TAPIR_YT_CLIENT_ID"} { if !strings.Contains(err.Error(), want) { t.Errorf("error %q must name missing field %q", err, want) } } } func TestValidateForAuth_PassesWhenComplete(t *testing.T) { c := Config{ YTClientID: "id", YTClientSecret: "secret", YTTokenRef: "youtube/refresh_token", SecretsFile: "/tmp/secrets.json", } if err := c.ValidateForAuth(); err != nil { t.Errorf("ValidateForAuth: unexpected error %v", err) } }