Skip to content

Commit 0a1ef36

Browse files
fix(config): treat empty env vars as unset in loadConfig
1 parent b9e9601 commit 0a1ef36

5 files changed

Lines changed: 56 additions & 6 deletions

File tree

src/commands/auth.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,6 +850,21 @@ describe('runWhoami', () => {
850850
expect(printed).toEqual(sampleMe);
851851
});
852852

853+
it('dry-run: whitespace-only TESTSPRITE_API_URL falls through to prod default endpoint', async () => {
854+
const { capture, deps } = makeCapture();
855+
await runWhoami(
856+
{ profile: 'default', output: 'text', debug: false, dryRun: true },
857+
{
858+
...deps,
859+
env: { TESTSPRITE_API_URL: ' ' },
860+
credentialsPath,
861+
fetchImpl: makeFetch(meResponse()),
862+
},
863+
);
864+
const out = capture.stdout.join('\n');
865+
expect(out).toContain('endpoint: https://api.testsprite.com');
866+
});
867+
853868
it('L1788: text output includes the resolved endpoint URL', async () => {
854869
writeProfile(
855870
'default',

src/commands/auth.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
readProfile,
1818
writeProfile,
1919
} from '../lib/credentials.js';
20-
import { loadConfig } from '../lib/config.js';
20+
import { loadConfig, normalizeEnvVar } from '../lib/config.js';
2121
import { emitDeprecationNotice } from '../lib/deprecate.js';
2222
import type { OutputMode } from '../lib/output.js';
2323
import { GLOBAL_OPTS_HINT, Output, resolveOutputMode } from '../lib/output.js';
@@ -85,7 +85,7 @@ export async function runConfigure(opts: ConfigureOptions, deps: AuthDeps = {}):
8585
// treated as unset. Without this, `''` (e.g. `export TESTSPRITE_API_URL=` in a
8686
// shell profile) is non-nullish and would short-circuit the `??` chains below to
8787
// an empty endpoint instead of falling through to the profile / prod default.
88-
const envApiUrl = env.TESTSPRITE_API_URL?.trim() || undefined;
88+
const envApiUrl = normalizeEnvVar(env.TESTSPRITE_API_URL);
8989

9090
// Dry-run: do not prompt, do not read env, do not write credentials.
9191
// Print the canned success shape so an agent sees exactly the JSON it
@@ -208,7 +208,8 @@ export async function runWhoami(opts: CommonOptions, deps: AuthDeps = {}): Promi
208208
// displayed URL always matches where requests actually go (dogfood L1788).
209209
let resolvedEndpoint: string;
210210
if (opts.dryRun) {
211-
resolvedEndpoint = opts.endpointUrl ?? env.TESTSPRITE_API_URL ?? 'https://api.testsprite.com';
211+
resolvedEndpoint =
212+
opts.endpointUrl ?? normalizeEnvVar(env.TESTSPRITE_API_URL) ?? 'https://api.testsprite.com';
212213
} else {
213214
const credentialsPath = deps.credentialsPath ?? defaultCredentialsPath();
214215
const config = loadConfig({

src/commands/init.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
parseRequestTimeoutFlag,
1616
type CommonOptions as FactoryCommonOptions,
1717
} from '../lib/client-factory.js';
18+
import { normalizeEnvVar } from '../lib/config.js';
1819
import { emitDeprecationNotice } from '../lib/deprecate.js';
1920
import { CLIError } from '../lib/errors.js';
2021
import { GLOBAL_OPTS_HINT, Output, resolveOutputMode } from '../lib/output.js';
@@ -39,7 +40,7 @@ const DEFAULT_API_URL = 'https://api.testsprite.com';
3940
*/
4041
function resolveReportedEndpoint(opts: InitOptions, deps: InitDeps): string {
4142
const env = deps.env ?? process.env;
42-
const envApiUrl = env.TESTSPRITE_API_URL?.trim() || undefined;
43+
const envApiUrl = normalizeEnvVar(env.TESTSPRITE_API_URL);
4344
let existing: string | undefined;
4445
try {
4546
existing = readProfile(opts.profile, { path: deps.credentialsPath })?.apiUrl;

src/lib/config.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,28 @@ describe('loadConfig', () => {
8181
const config = loadConfig({ profile: 'dev', env: {}, credentialsPath });
8282
expect(config.apiKey).toBe('sk-dev');
8383
});
84+
85+
it('treats empty / whitespace TESTSPRITE_API_URL as unset (falls through to profile)', () => {
86+
writeProfile(
87+
'default',
88+
{ apiKey: 'sk-file', apiUrl: 'https://api.example.com:8443' },
89+
{ path: credentialsPath },
90+
);
91+
const config = loadConfig({
92+
env: { TESTSPRITE_API_URL: ' ' },
93+
credentialsPath,
94+
});
95+
expect(config.apiUrl).toBe('https://api.example.com:8443');
96+
});
97+
98+
it('treats empty / whitespace TESTSPRITE_API_KEY as unset (falls through to profile)', () => {
99+
writeProfile('default', { apiKey: 'sk-file' }, { path: credentialsPath });
100+
const config = loadConfig({
101+
env: { TESTSPRITE_API_KEY: '' },
102+
credentialsPath,
103+
});
104+
expect(config.apiKey).toBe('sk-file');
105+
});
84106
});
85107

86108
describe('defaultConfigPath', () => {

src/lib/config.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export interface LoadConfigOptions {
1717

1818
const DEFAULT_API_URL = 'https://api.testsprite.com';
1919

20+
/** Treat empty / whitespace-only env values as unset for `??` resolution chains. */
21+
export function normalizeEnvVar(value: string | undefined): string | undefined {
22+
return value?.trim() || undefined;
23+
}
24+
2025
export function defaultConfigPath(): string {
2126
return join(homedir(), '.testsprite', 'config');
2227
}
@@ -38,9 +43,15 @@ export function loadConfig(options: LoadConfigOptions = {}): Config {
3843
const credentialsPath = options.credentialsPath ?? defaultCredentialsPath();
3944
const fileEntry = readProfile(profile, { path: credentialsPath });
4045

46+
// Empty / whitespace-only env vars are treated as unset so they do not
47+
// short-circuit the `??` chain (e.g. `export TESTSPRITE_API_URL=` in a shell
48+
// profile). Matches the normalization in auth configure and init/setup.
49+
const envApiUrl = normalizeEnvVar(env.TESTSPRITE_API_URL);
50+
const envApiKey = normalizeEnvVar(env.TESTSPRITE_API_KEY);
51+
4152
return {
42-
apiUrl: options.endpointUrl ?? env.TESTSPRITE_API_URL ?? fileEntry?.apiUrl ?? DEFAULT_API_URL,
43-
apiKey: env.TESTSPRITE_API_KEY ?? fileEntry?.apiKey,
53+
apiUrl: options.endpointUrl ?? envApiUrl ?? fileEntry?.apiUrl ?? DEFAULT_API_URL,
54+
apiKey: envApiKey ?? fileEntry?.apiKey,
4455
profile,
4556
};
4657
}

0 commit comments

Comments
 (0)