Skip to content

Commit 83afedb

Browse files
committed
fix(test): use Proxy instead of Object.defineProperty for env var lock
Object.defineProperty with configurable:true is bypassed by `delete` (which concurrent test files do in afterEach), and configurable:false causes `delete` to throw in Bun. Switch to a Proxy on process.env that intercepts get/set/delete of SENTRY_CONFIG_DIR — survives both `delete` and re-assignment from concurrent test files.
1 parent 0a24dc5 commit 83afedb

File tree

1 file changed

+62
-24
lines changed

1 file changed

+62
-24
lines changed

test/helpers.ts

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,49 +74,87 @@ export function mockFetch(fn: FetchMockFn): typeof fetch {
7474
return fn as unknown as typeof fetch;
7575
}
7676

77+
/**
78+
* Module-level lock state for SENTRY_CONFIG_DIR.
79+
*
80+
* We replace process.env with a Proxy that intercepts reads, writes,
81+
* and deletes of SENTRY_CONFIG_DIR. When locked, the proxy returns the
82+
* locked value and silently ignores mutations. When unlocked, all
83+
* operations pass through to the real env object.
84+
*
85+
* A Proxy is used instead of Object.defineProperty because:
86+
* - configurable:true descriptors can be removed by `delete` (other
87+
* test files do `delete process.env[CONFIG_DIR_ENV_VAR]` in afterEach)
88+
* - configurable:false descriptors cause `delete` to throw in Bun,
89+
* breaking other test files
90+
*
91+
* The Proxy is installed once and persists for the process lifetime.
92+
* Lock/unlock just toggles the module-level state variables.
93+
*/
94+
let configDirLocked = false;
95+
let configDirLockedValue: string | undefined;
96+
let envProxyInstalled = false;
97+
98+
function installEnvProxy(): void {
99+
if (envProxyInstalled) return;
100+
const realEnv = process.env;
101+
process.env = new Proxy(realEnv, {
102+
get(target, prop, receiver) {
103+
if (prop === "SENTRY_CONFIG_DIR" && configDirLocked) {
104+
return configDirLockedValue;
105+
}
106+
return Reflect.get(target, prop, receiver);
107+
},
108+
set(target, prop, value) {
109+
if (prop === "SENTRY_CONFIG_DIR" && configDirLocked) {
110+
return true; // Silently ignore
111+
}
112+
return Reflect.set(target, prop, value);
113+
},
114+
deleteProperty(target, prop) {
115+
if (prop === "SENTRY_CONFIG_DIR" && configDirLocked) {
116+
return true; // Silently ignore
117+
}
118+
return Reflect.deleteProperty(target, prop);
119+
},
120+
});
121+
envProxyInstalled = true;
122+
}
123+
77124
/**
78125
* Lock SENTRY_CONFIG_DIR so concurrent test files cannot change it.
79126
*
80127
* Bun runs test files concurrently in a single process, sharing
81-
* `process.env` and `globalThis.fetch`. Between any `await` point
82-
* inside our test, another file's beforeEach/afterEach can mutate
83-
* these globals. The DB singleton auto-invalidates when the config
84-
* dir changes, so even a momentary mutation causes getDatabase() to
85-
* open the wrong DB and lose auth tokens / defaults.
128+
* `process.env`. Between any `await` point inside our test, another
129+
* file's beforeEach/afterEach can mutate the env var. The DB singleton
130+
* auto-invalidates when the config dir changes, so even a momentary
131+
* mutation causes getDatabase() to open the wrong DB and lose data.
86132
*
87-
* Uses Object.defineProperty to make the env var return the locked
88-
* value regardless of what other tests write. Call `unlockConfigDir`
89-
* in afterEach so other tests can proceed normally.
133+
* Uses a Proxy on process.env that intercepts get/set/delete of the
134+
* config dir env var. When locked, returns the locked value and ignores
135+
* mutations. When unlocked, all operations pass through normally.
90136
*
91137
* @param configDir - The config directory path to lock
92138
* @returns Unlock function to call in afterEach
93139
*/
94140
export function lockConfigDir(configDir: string): () => void {
95-
Object.defineProperty(process.env, "SENTRY_CONFIG_DIR", {
96-
get() {
97-
return configDir;
98-
},
99-
set() {
100-
// Silently ignore writes from other test files
101-
},
102-
configurable: true,
103-
enumerable: true,
104-
});
141+
installEnvProxy();
142+
configDirLocked = true;
143+
configDirLockedValue = configDir;
105144

106145
return () => {
107-
// Restore normal property behavior
108-
delete process.env.SENTRY_CONFIG_DIR;
109-
process.env.SENTRY_CONFIG_DIR = configDir;
146+
configDirLocked = false;
110147
};
111148
}
112149

113150
/**
114151
* Lock globalThis.fetch to a mock handler so concurrent test files
115152
* cannot replace it between async boundaries.
116153
*
117-
* Uses Object.defineProperty to intercept writes to globalThis.fetch,
118-
* similar to lockConfigDir. Call the returned unlock function in
119-
* afterEach to restore normal behavior.
154+
* Uses configurable: true since concurrent test files use assignment
155+
* (not delete) for fetch, and other test files (e.g. api-client) need
156+
* to assign their own mock via globalThis.fetch = ... without going
157+
* through this lock.
120158
*
121159
* @param fn - The fetch mock implementation
122160
* @returns Unlock function to call in afterEach

0 commit comments

Comments
 (0)