Proxy: Make internal/env tests hermetic by injecting env and file indirections

Swap direct os.Getenv/Setenv/LookupEnv and os.Open calls in internal/env
for package-level function variables so tests can replace them with
in-memory fakes. Tests no longer mutate process env or hit the filesystem,
making them safe to run in parallel and free of global side effects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
winlin 2026-04-18 14:53:44 -04:00
parent 41ad82e19f
commit 431b842dd1
2 changed files with 208 additions and 197 deletions

42
internal/env/env.go vendored
View File

@ -6,6 +6,7 @@ package env
import (
"bufio"
"context"
"io"
"os"
"strings"
@ -13,6 +14,17 @@ import (
"srsx/internal/logger"
)
// Indirections over os and filesystem primitives so tests can swap them
// without touching real process env or the filesystem.
var (
getEnv = os.Getenv
setEnv = os.Setenv
lookupEnv = os.LookupEnv
openFile = func(name string) (io.ReadCloser, error) {
return os.Open(name)
}
)
// Environment provides access to environment variables.
type Environment interface {
// Go pprof profiling
@ -171,16 +183,10 @@ func loadEnvFile(ctx context.Context) error {
return errors.Wrapf(err, "load .env file")
}
// Build a set of existing environment variable keys, so we don't overwrite them.
currentEnv := make(map[string]bool)
for _, entry := range os.Environ() {
key, _, _ := strings.Cut(entry, "=")
currentEnv[key] = true
}
// Skip keys already set in the environment so we don't overwrite them.
for key, value := range envMap {
if !currentEnv[key] {
os.Setenv(key, value)
if _, ok := lookupEnv(key); !ok {
setEnv(key, value)
}
}
@ -188,16 +194,21 @@ func loadEnvFile(ctx context.Context) error {
return nil
}
// parseEnvFile reads a .env file and returns a map of key-value pairs.
// parseEnvFile opens filename and parses its contents as .env-formatted lines.
func parseEnvFile(filename string) (map[string]string, error) {
file, err := os.Open(filename)
file, err := openFile(filename)
if err != nil {
return nil, err
}
defer file.Close()
return parseEnvReader(file)
}
// parseEnvReader parses .env-formatted content from r. It performs no I/O
// beyond reading r, so it is trivially testable with strings.NewReader.
func parseEnvReader(r io.Reader) (map[string]string, error) {
envMap := make(map[string]string)
scanner := bufio.NewScanner(file)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
@ -330,11 +341,6 @@ func buildDefaultEnvironmentVariables(ctx context.Context) {
// setEnvDefault set env key=value if not set.
func setEnvDefault(key, value string) {
if getEnv(key) == "" {
os.Setenv(key, value)
setEnv(key, value)
}
}
// getEnv get the env by key.
func getEnv(key string) string {
return os.Getenv(key)
}

View File

@ -5,14 +5,58 @@ package env
import (
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
srserrors "srsx/internal/errors"
)
func TestParseEnvFile_BasicKeyValue(t *testing.T) {
f := writeTempEnv(t, "FOO=bar\nHELLO=world\n")
m, err := parseEnvFile(f)
// fakeEnv is an in-memory replacement for process environment variables.
// Tests install it via withFakeEnv so no real os.Setenv/os.Getenv call is
// ever made, which keeps tests hermetic and free of global side effects.
type fakeEnv struct {
store map[string]string
}
func (f *fakeEnv) get(k string) string { return f.store[k] }
func (f *fakeEnv) set(k, v string) error { f.store[k] = v; return nil }
func (f *fakeEnv) lookup(k string) (string, bool) {
v, ok := f.store[k]
return v, ok
}
// withFakeEnv swaps getEnv/setEnv/lookupEnv to an in-memory map for the
// duration of the test and restores the originals on cleanup.
func withFakeEnv(t *testing.T) *fakeEnv {
t.Helper()
fe := &fakeEnv{store: map[string]string{}}
origGet, origSet, origLookup := getEnv, setEnv, lookupEnv
getEnv, setEnv, lookupEnv = fe.get, fe.set, fe.lookup
t.Cleanup(func() {
getEnv, setEnv, lookupEnv = origGet, origSet, origLookup
})
return fe
}
// withFakeOpen swaps openFile to return either content or err, and
// restores the original on cleanup. If err is non-nil, content is ignored.
func withFakeOpen(t *testing.T, content string, err error) {
t.Helper()
orig := openFile
openFile = func(string) (io.ReadCloser, error) {
if err != nil {
return nil, err
}
return io.NopCloser(strings.NewReader(content)), nil
}
t.Cleanup(func() { openFile = orig })
}
func TestParseEnvReader_BasicKeyValue(t *testing.T) {
m, err := parseEnvReader(strings.NewReader("FOO=bar\nHELLO=world\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -24,9 +68,8 @@ func TestParseEnvFile_BasicKeyValue(t *testing.T) {
}
}
func TestParseEnvFile_SkipCommentsAndBlankLines(t *testing.T) {
f := writeTempEnv(t, "# this is a comment\n\nKEY=value\n\n# another comment\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_SkipCommentsAndBlankLines(t *testing.T) {
m, err := parseEnvReader(strings.NewReader("# this is a comment\n\nKEY=value\n\n# another comment\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -38,9 +81,8 @@ func TestParseEnvFile_SkipCommentsAndBlankLines(t *testing.T) {
}
}
func TestParseEnvFile_ExportPrefix(t *testing.T) {
f := writeTempEnv(t, "export PORT=8080\nexport HOST=localhost\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_ExportPrefix(t *testing.T) {
m, err := parseEnvReader(strings.NewReader("export PORT=8080\nexport HOST=localhost\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -52,24 +94,21 @@ func TestParseEnvFile_ExportPrefix(t *testing.T) {
}
}
func TestParseEnvFile_SingleQuoted(t *testing.T) {
f := writeTempEnv(t, "KEY='hello world'\nRAW='no\\nescaping'\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_SingleQuoted(t *testing.T) {
m, err := parseEnvReader(strings.NewReader("KEY='hello world'\nRAW='no\\nescaping'\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if m["KEY"] != "hello world" {
t.Errorf("KEY = %q, want %q", m["KEY"], "hello world")
}
// Single quotes: backslash-n stays literal.
if m["RAW"] != `no\nescaping` {
t.Errorf("RAW = %q, want %q", m["RAW"], `no\nescaping`)
}
}
func TestParseEnvFile_DoubleQuoted(t *testing.T) {
f := writeTempEnv(t, `KEY="hello world"`+"\n"+`MSG="line1\nline2"`+"\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_DoubleQuoted(t *testing.T) {
m, err := parseEnvReader(strings.NewReader(`KEY="hello world"` + "\n" + `MSG="line1\nline2"` + "\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -81,9 +120,8 @@ func TestParseEnvFile_DoubleQuoted(t *testing.T) {
}
}
func TestParseEnvFile_DoubleQuotedEscapes(t *testing.T) {
f := writeTempEnv(t, `KEY="say \"hi\""`+"\n"+`BS="back\\slash"`+"\n"+`CR="a\rb"`+"\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_DoubleQuotedEscapes(t *testing.T) {
m, err := parseEnvReader(strings.NewReader(`KEY="say \"hi\""` + "\n" + `BS="back\\slash"` + "\n" + `CR="a\rb"` + "\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -98,9 +136,8 @@ func TestParseEnvFile_DoubleQuotedEscapes(t *testing.T) {
}
}
func TestParseEnvFile_InlineComment(t *testing.T) {
f := writeTempEnv(t, "KEY=value # this is a comment\nNUM=42 # the answer\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_InlineComment(t *testing.T) {
m, err := parseEnvReader(strings.NewReader("KEY=value # this is a comment\nNUM=42 # the answer\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -112,9 +149,8 @@ func TestParseEnvFile_InlineComment(t *testing.T) {
}
}
func TestParseEnvFile_NoEqualsSign(t *testing.T) {
f := writeTempEnv(t, "NOEQUALS\nKEY=value\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_NoEqualsSign(t *testing.T) {
m, err := parseEnvReader(strings.NewReader("NOEQUALS\nKEY=value\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -126,9 +162,8 @@ func TestParseEnvFile_NoEqualsSign(t *testing.T) {
}
}
func TestParseEnvFile_EmptyValue(t *testing.T) {
f := writeTempEnv(t, "KEY=\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_EmptyValue(t *testing.T) {
m, err := parseEnvReader(strings.NewReader("KEY=\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -137,9 +172,8 @@ func TestParseEnvFile_EmptyValue(t *testing.T) {
}
}
func TestParseEnvFile_ValueWithEquals(t *testing.T) {
f := writeTempEnv(t, "URL=postgres://host:5432/db?opt=val\n")
m, err := parseEnvFile(f)
func TestParseEnvReader_ValueWithEquals(t *testing.T) {
m, err := parseEnvReader(strings.NewReader("URL=postgres://host:5432/db?opt=val\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -148,19 +182,8 @@ func TestParseEnvFile_ValueWithEquals(t *testing.T) {
}
}
func TestParseEnvFile_FileNotFound(t *testing.T) {
_, err := parseEnvFile("/nonexistent/.env")
if err == nil {
t.Fatal("expected error for missing file")
}
if !os.IsNotExist(err) {
t.Errorf("expected os.IsNotExist, got: %v", err)
}
}
func TestParseEnvFile_WhitespaceAroundKeyValue(t *testing.T) {
f := writeTempEnv(t, " KEY = value \n")
m, err := parseEnvFile(f)
func TestParseEnvReader_WhitespaceAroundKeyValue(t *testing.T) {
m, err := parseEnvReader(strings.NewReader(" KEY = value \n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -169,32 +192,9 @@ func TestParseEnvFile_WhitespaceAroundKeyValue(t *testing.T) {
}
}
func TestLoadEnvFile_DoesNotOverwriteExisting(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte("TEST_EXISTING=fromfile\nTEST_NEW=fromfile\n"), 0644); err != nil {
t.Fatalf("write .env: %v", err)
}
t.Chdir(dir)
t.Setenv("TEST_EXISTING", "fromshell")
unsetEnv(t, "TEST_NEW")
if err := loadEnvFile(context.Background()); err != nil {
t.Fatalf("loadEnvFile: %v", err)
}
if got := os.Getenv("TEST_EXISTING"); got != "fromshell" {
t.Errorf("TEST_EXISTING = %q, want %q (should not overwrite)", got, "fromshell")
}
if got := os.Getenv("TEST_NEW"); got != "fromfile" {
t.Errorf("TEST_NEW = %q, want %q", got, "fromfile")
}
}
func TestParseEnvFile_ShortValue(t *testing.T) {
func TestParseEnvReader_ShortValue(t *testing.T) {
// Single-character value exercises the len(value) < 2 short-value branch.
f := writeTempEnv(t, "A=x\nB=y\n")
m, err := parseEnvFile(f)
m, err := parseEnvReader(strings.NewReader("A=x\nB=y\n"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -206,121 +206,144 @@ func TestParseEnvFile_ShortValue(t *testing.T) {
}
}
func TestSetEnvDefault_SetsWhenEmpty(t *testing.T) {
const key = "TEST_SET_ENV_DEFAULT_EMPTY"
// t.Setenv both guards against t.Parallel and auto-restores the
// original value on cleanup. setEnvDefault treats "" as unset.
t.Setenv(key, "")
setEnvDefault(key, "defaultVal")
if got := os.Getenv(key); got != "defaultVal" {
t.Errorf("%s = %q, want %q", key, got, "defaultVal")
func TestParseEnvFile_FileNotFound(t *testing.T) {
withFakeOpen(t, "", os.ErrNotExist)
_, err := parseEnvFile(".env")
if !errors.Is(err, os.ErrNotExist) {
t.Errorf("expected os.ErrNotExist, got: %v", err)
}
}
func TestSetEnvDefault_PreservesExisting(t *testing.T) {
const key = "TEST_SET_ENV_DEFAULT_EXISTING"
t.Setenv(key, "original")
setEnvDefault(key, "shouldNotApply")
if got := os.Getenv(key); got != "original" {
t.Errorf("%s = %q, want %q", key, got, "original")
func TestParseEnvFile_OpenError(t *testing.T) {
// A non-NotExist open error should bubble up as-is.
sentinel := errors.New("boom")
withFakeOpen(t, "", sentinel)
_, err := parseEnvFile(".env")
if !errors.Is(err, sentinel) {
t.Errorf("expected sentinel error, got: %v", err)
}
}
func TestParseEnvFile_DelegatesToReader(t *testing.T) {
withFakeOpen(t, "FOO=bar\n", nil)
m, err := parseEnvFile(".env")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if m["FOO"] != "bar" {
t.Errorf("FOO = %q, want %q", m["FOO"], "bar")
}
}
func TestLoadEnvFile_DoesNotOverwriteExisting(t *testing.T) {
fe := withFakeEnv(t)
fe.store["TEST_EXISTING"] = "fromshell"
// TEST_NEW is absent from the store, so it should be loaded from the file.
withFakeOpen(t, "TEST_EXISTING=fromfile\nTEST_NEW=fromfile\n", nil)
if err := loadEnvFile(context.Background()); err != nil {
t.Fatalf("loadEnvFile: %v", err)
}
if got := fe.store["TEST_EXISTING"]; got != "fromshell" {
t.Errorf("TEST_EXISTING = %q, want %q (should not overwrite)", got, "fromshell")
}
if got := fe.store["TEST_NEW"]; got != "fromfile" {
t.Errorf("TEST_NEW = %q, want %q", got, "fromfile")
}
}
func TestLoadEnvFile_NoFileIsNoError(t *testing.T) {
// Run in a temp dir with no .env present.
t.Chdir(t.TempDir())
withFakeEnv(t)
withFakeOpen(t, "", os.ErrNotExist)
if err := loadEnvFile(context.Background()); err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestLoadEnvFile_AppliesFromFile(t *testing.T) {
const key = "TEST_LOAD_ENV_FILE_APPLIES"
unsetEnv(t, key)
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte(key+"=loaded\n"), 0644); err != nil {
t.Fatalf("write .env: %v", err)
func TestLoadEnvFile_OpenErrorIsWrapped(t *testing.T) {
withFakeEnv(t)
sentinel := errors.New("disk gone")
withFakeOpen(t, "", sentinel)
err := loadEnvFile(context.Background())
if err == nil {
t.Fatal("expected error, got nil")
}
t.Chdir(dir)
if srserrors.Cause(err) != sentinel {
t.Errorf("expected wrapped sentinel, got: %v", err)
}
}
func TestLoadEnvFile_AppliesFromFile(t *testing.T) {
fe := withFakeEnv(t)
withFakeOpen(t, "TEST_LOAD_ENV_FILE_APPLIES=loaded\n", nil)
if err := loadEnvFile(context.Background()); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := os.Getenv(key); got != "loaded" {
t.Errorf("%s = %q, want %q", key, got, "loaded")
if got := fe.store["TEST_LOAD_ENV_FILE_APPLIES"]; got != "loaded" {
t.Errorf("got %q, want %q", got, "loaded")
}
}
func TestSetEnvDefault_SetsWhenEmpty(t *testing.T) {
fe := withFakeEnv(t)
// Key is absent (getEnv returns ""), so the default should apply.
setEnvDefault("KEY", "defaultVal")
if got := fe.store["KEY"]; got != "defaultVal" {
t.Errorf("KEY = %q, want %q", got, "defaultVal")
}
}
func TestSetEnvDefault_PreservesExisting(t *testing.T) {
fe := withFakeEnv(t)
fe.store["KEY"] = "original"
setEnvDefault("KEY", "shouldNotApply")
if got := fe.store["KEY"]; got != "original" {
t.Errorf("KEY = %q, want %q", got, "original")
}
}
func TestNewEnvironment_AppliesDefaultsAndAccessors(t *testing.T) {
// Work in a clean dir so no stray .env is picked up.
t.Chdir(t.TempDir())
withFakeEnv(t)
// No .env file present.
withFakeOpen(t, "", os.ErrNotExist)
// Keys with a default in buildDefaultEnvironmentVariables.
defaults := map[string]string{
"GO_PPROF": "",
"PROXY_FORCE_QUIT_TIMEOUT": "30s",
"PROXY_GRACE_QUIT_TIMEOUT": "20s",
"PROXY_HTTP_API": "11985",
"PROXY_HTTP_SERVER": "18080",
"PROXY_RTMP_SERVER": "11935",
"PROXY_WEBRTC_SERVER": "18000",
"PROXY_SRT_SERVER": "20080",
"PROXY_SYSTEM_API": "12025",
"PROXY_STATIC_FILES": "./trunk/research",
"PROXY_LOAD_BALANCER_TYPE": "memory",
"PROXY_REDIS_HOST": "127.0.0.1",
"PROXY_REDIS_PORT": "6379",
"PROXY_REDIS_PASSWORD": "",
"PROXY_REDIS_DB": "0",
"PROXY_DEFAULT_BACKEND_ENABLED": "off",
"PROXY_DEFAULT_BACKEND_IP": "127.0.0.1",
"PROXY_DEFAULT_BACKEND_RTMP": "1935",
"PROXY_DEFAULT_BACKEND_API": "1985",
"PROXY_DEFAULT_BACKEND_RTC": "8000",
"PROXY_DEFAULT_BACKEND_SRT": "10080",
}
// Clear defaults so setEnvDefault actually applies them.
for k := range defaults {
t.Setenv(k, "")
}
// PROXY_DEFAULT_BACKEND_HTTP has no default; set it explicitly so the
// accessor has a value to return.
t.Setenv("PROXY_DEFAULT_BACKEND_HTTP", "8080")
// PROXY_DEFAULT_BACKEND_HTTP has no default in buildDefaultEnvironmentVariables;
// pre-set it so the accessor has a value to return.
setEnv("PROXY_DEFAULT_BACKEND_HTTP", "8080")
env, err := NewEnvironment(context.Background())
if err != nil {
t.Fatalf("NewEnvironment: %v", err)
}
// Verify every accessor returns the expected value.
cases := []struct {
name string
got string
want string
}{
{"GoPprof", env.GoPprof(), defaults["GO_PPROF"]},
{"GraceQuitTimeout", env.GraceQuitTimeout(), defaults["PROXY_GRACE_QUIT_TIMEOUT"]},
{"ForceQuitTimeout", env.ForceQuitTimeout(), defaults["PROXY_FORCE_QUIT_TIMEOUT"]},
{"HttpAPI", env.HttpAPI(), defaults["PROXY_HTTP_API"]},
{"HttpServer", env.HttpServer(), defaults["PROXY_HTTP_SERVER"]},
{"RtmpServer", env.RtmpServer(), defaults["PROXY_RTMP_SERVER"]},
{"WebRTCServer", env.WebRTCServer(), defaults["PROXY_WEBRTC_SERVER"]},
{"SRTServer", env.SRTServer(), defaults["PROXY_SRT_SERVER"]},
{"SystemAPI", env.SystemAPI(), defaults["PROXY_SYSTEM_API"]},
{"StaticFiles", env.StaticFiles(), defaults["PROXY_STATIC_FILES"]},
{"LoadBalancerType", env.LoadBalancerType(), defaults["PROXY_LOAD_BALANCER_TYPE"]},
{"RedisHost", env.RedisHost(), defaults["PROXY_REDIS_HOST"]},
{"RedisPort", env.RedisPort(), defaults["PROXY_REDIS_PORT"]},
{"RedisPassword", env.RedisPassword(), defaults["PROXY_REDIS_PASSWORD"]},
{"RedisDB", env.RedisDB(), defaults["PROXY_REDIS_DB"]},
{"DefaultBackendEnabled", env.DefaultBackendEnabled(), defaults["PROXY_DEFAULT_BACKEND_ENABLED"]},
{"DefaultBackendIP", env.DefaultBackendIP(), defaults["PROXY_DEFAULT_BACKEND_IP"]},
{"DefaultBackendRTMP", env.DefaultBackendRTMP(), defaults["PROXY_DEFAULT_BACKEND_RTMP"]},
{"GoPprof", env.GoPprof(), ""},
{"GraceQuitTimeout", env.GraceQuitTimeout(), "20s"},
{"ForceQuitTimeout", env.ForceQuitTimeout(), "30s"},
{"HttpAPI", env.HttpAPI(), "11985"},
{"HttpServer", env.HttpServer(), "18080"},
{"RtmpServer", env.RtmpServer(), "11935"},
{"WebRTCServer", env.WebRTCServer(), "18000"},
{"SRTServer", env.SRTServer(), "20080"},
{"SystemAPI", env.SystemAPI(), "12025"},
{"StaticFiles", env.StaticFiles(), "./trunk/research"},
{"LoadBalancerType", env.LoadBalancerType(), "memory"},
{"RedisHost", env.RedisHost(), "127.0.0.1"},
{"RedisPort", env.RedisPort(), "6379"},
{"RedisPassword", env.RedisPassword(), ""},
{"RedisDB", env.RedisDB(), "0"},
{"DefaultBackendEnabled", env.DefaultBackendEnabled(), "off"},
{"DefaultBackendIP", env.DefaultBackendIP(), "127.0.0.1"},
{"DefaultBackendRTMP", env.DefaultBackendRTMP(), "1935"},
{"DefaultBackendHttp", env.DefaultBackendHttp(), "8080"},
{"DefaultBackendAPI", env.DefaultBackendAPI(), defaults["PROXY_DEFAULT_BACKEND_API"]},
{"DefaultBackendRTC", env.DefaultBackendRTC(), defaults["PROXY_DEFAULT_BACKEND_RTC"]},
{"DefaultBackendSRT", env.DefaultBackendSRT(), defaults["PROXY_DEFAULT_BACKEND_SRT"]},
{"DefaultBackendAPI", env.DefaultBackendAPI(), "1985"},
{"DefaultBackendRTC", env.DefaultBackendRTC(), "8000"},
{"DefaultBackendSRT", env.DefaultBackendSRT(), "10080"},
}
for _, c := range cases {
if c.got != c.want {
@ -330,9 +353,9 @@ func TestNewEnvironment_AppliesDefaultsAndAccessors(t *testing.T) {
}
func TestNewEnvironment_PreservesPreSetValues(t *testing.T) {
// When a var is already set, setEnvDefault must not overwrite it.
t.Chdir(t.TempDir())
t.Setenv("PROXY_HTTP_API", "9999")
withFakeEnv(t)
withFakeOpen(t, "", os.ErrNotExist)
setEnv("PROXY_HTTP_API", "9999")
env, err := NewEnvironment(context.Background())
if err != nil {
@ -343,31 +366,13 @@ func TestNewEnvironment_PreservesPreSetValues(t *testing.T) {
}
}
// unsetEnv removes key for the duration of the test and restores its
// original value (or unset state) on cleanup. It also calls t.Setenv on
// a sentinel so the test will panic if anyone adds t.Parallel() — env
// vars are process-global and cannot be mutated safely in parallel.
func unsetEnv(t *testing.T, key string) {
t.Helper()
t.Setenv("_ENV_TEST_NO_PARALLEL", "1")
prev, had := os.LookupEnv(key)
os.Unsetenv(key)
t.Cleanup(func() {
if had {
os.Setenv(key, prev)
} else {
os.Unsetenv(key)
}
})
}
func TestNewEnvironment_LoadEnvFailurePropagates(t *testing.T) {
withFakeEnv(t)
sentinel := errors.New("open failed")
withFakeOpen(t, "", sentinel)
// writeTempEnv writes content to a temp .env file and returns the path.
func writeTempEnv(t *testing.T, content string) string {
t.Helper()
dir := t.TempDir()
f := filepath.Join(dir, ".env")
if err := os.WriteFile(f, []byte(content), 0644); err != nil {
t.Fatalf("write temp .env: %v", err)
_, err := NewEnvironment(context.Background())
if srserrors.Cause(err) != sentinel {
t.Errorf("expected wrapped sentinel, got: %v", err)
}
return f
}