mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-01 21:30:21 +00:00
test: add 55 tests for credential management functions in shared/common.sh (#714)
Add comprehensive test coverage for the untested credential management pipeline (_load_token_from_env, _load_token_from_config, _validate_token_with_provider, _save_token_to_config, _multi_creds_all_env_set, _multi_creds_load_config, _multi_creds_validate) plus save/load roundtrip integration tests. These functions are used by every cloud provider script but had zero test coverage. Tests run in real bash subprocesses sourcing shared/common.sh to catch actual shell behavior. Agent: test-engineer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
26829b1065
commit
6ca02f9362
1 changed files with 733 additions and 0 deletions
733
cli/src/__tests__/shared-common-credential-mgmt.test.ts
Normal file
733
cli/src/__tests__/shared-common-credential-mgmt.test.ts
Normal file
|
|
@ -0,0 +1,733 @@
|
|||
import { describe, it, expect, afterEach } from "bun:test";
|
||||
import { execSync } from "child_process";
|
||||
import { resolve, join } from "path";
|
||||
import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync, statSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
|
||||
/**
|
||||
* Tests for credential management functions in shared/common.sh.
|
||||
*
|
||||
* These functions had zero test coverage despite being used by every cloud
|
||||
* provider script. They handle API token loading from env vars, config files,
|
||||
* validation via provider test functions, and saving with proper permissions.
|
||||
*
|
||||
* Functions tested:
|
||||
* - _load_token_from_env: load token from environment variable
|
||||
* - _load_token_from_config: load token from JSON config file (api_key or token field)
|
||||
* - _validate_token_with_provider: validate token via a test function
|
||||
* - _save_token_to_config: save token to JSON config with chmod 600
|
||||
* - _multi_creds_all_env_set: check if all env vars are set
|
||||
* - _multi_creds_load_config: load multiple credentials from JSON config
|
||||
* - _multi_creds_validate: validate credentials via test function, unset on failure
|
||||
*
|
||||
* Agent: test-engineer
|
||||
*/
|
||||
|
||||
const REPO_ROOT = resolve(import.meta.dir, "../../..");
|
||||
const COMMON_SH = resolve(REPO_ROOT, "shared/common.sh");
|
||||
|
||||
/**
|
||||
* Run a bash snippet that sources shared/common.sh first.
|
||||
* Returns { exitCode, stdout, stderr }.
|
||||
*/
|
||||
function runBash(script: string): { exitCode: number; stdout: string; stderr: string } {
|
||||
const fullScript = `source "${COMMON_SH}"\n${script}`;
|
||||
try {
|
||||
const stdout = execSync(`bash -c '${fullScript.replace(/'/g, "'\\''")}'`, {
|
||||
encoding: "utf-8",
|
||||
timeout: 10000,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
return { exitCode: 0, stdout: stdout.trim(), stderr: "" };
|
||||
} catch (err: any) {
|
||||
return {
|
||||
exitCode: err.status ?? 1,
|
||||
stdout: (err.stdout || "").trim(),
|
||||
stderr: (err.stderr || "").trim(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createTempDir(): string {
|
||||
const dir = join(tmpdir(), `spawn-cred-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs) {
|
||||
if (existsSync(dir)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
tempDirs.length = 0;
|
||||
});
|
||||
|
||||
function trackTempDir(): string {
|
||||
const dir = createTempDir();
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
// ── _load_token_from_env ──────────────────────────────────────────────────
|
||||
|
||||
describe("_load_token_from_env", () => {
|
||||
it("should return 0 when env var is set", () => {
|
||||
const result = runBash(`
|
||||
export MY_TOKEN="test-token-123"
|
||||
_load_token_from_env MY_TOKEN "TestProvider"
|
||||
echo "exit=$?"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 1 when env var is not set", () => {
|
||||
const result = runBash(`
|
||||
unset MY_TOKEN 2>/dev/null
|
||||
_load_token_from_env MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 when env var is empty string", () => {
|
||||
const result = runBash(`
|
||||
export MY_TOKEN=""
|
||||
_load_token_from_env MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should log info message when token found", () => {
|
||||
const result = runBash(`
|
||||
export MY_TOKEN="abc"
|
||||
_load_token_from_env MY_TOKEN "Hetzner" 2>&1
|
||||
`);
|
||||
expect(result.stdout).toContain("Hetzner");
|
||||
expect(result.stdout).toContain("environment");
|
||||
});
|
||||
|
||||
it("should work with different env var names", () => {
|
||||
const result = runBash(`
|
||||
export HCLOUD_TOKEN="hetzner-token-value"
|
||||
_load_token_from_env HCLOUD_TOKEN "Hetzner Cloud"
|
||||
echo "exit=$?"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle tokens with special characters", () => {
|
||||
const result = runBash(`
|
||||
export MY_TOKEN="sk-or-v1-abc123/def+ghi="
|
||||
_load_token_from_env MY_TOKEN "Provider"
|
||||
echo "exit=$?"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── _load_token_from_config ───────────────────────────────────────────────
|
||||
|
||||
describe("_load_token_from_config", () => {
|
||||
it("should load token from api_key field in JSON config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ api_key: "my-api-key-123" }));
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "TestProvider"
|
||||
echo "$MY_TOKEN"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("my-api-key-123");
|
||||
});
|
||||
|
||||
it("should load token from token field in JSON config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ token: "my-token-456" }));
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "TestProvider"
|
||||
echo "$MY_TOKEN"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("my-token-456");
|
||||
});
|
||||
|
||||
it("should prefer api_key over token when both present", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ api_key: "api-key-value", token: "token-value" }));
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "TestProvider"
|
||||
echo "$MY_TOKEN"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("api-key-value");
|
||||
});
|
||||
|
||||
it("should return 1 when config file does not exist", () => {
|
||||
const result = runBash(`
|
||||
_load_token_from_config "/nonexistent/path/config.json" MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 when config file has empty api_key and empty token", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ api_key: "", token: "" }));
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 for invalid JSON", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, "not valid json {{{");
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 when JSON has no api_key or token field", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ username: "user", password: "pass" }));
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should export the env var with the loaded value", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ api_key: "loaded-token" }));
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" HCLOUD_TOKEN "Hetzner"
|
||||
echo "HCLOUD_TOKEN=$HCLOUD_TOKEN"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("HCLOUD_TOKEN=loaded-token");
|
||||
});
|
||||
|
||||
it("should log info message with config file path", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ api_key: "test" }));
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "TestProvider" 2>&1
|
||||
`);
|
||||
expect(result.stdout).toContain(configFile);
|
||||
expect(result.stdout).toContain("TestProvider");
|
||||
});
|
||||
|
||||
it("should fall back to token field when api_key is empty", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ api_key: "", token: "fallback-token" }));
|
||||
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "TestProvider"
|
||||
echo "$MY_TOKEN"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("fallback-token");
|
||||
});
|
||||
});
|
||||
|
||||
// ── _validate_token_with_provider ─────────────────────────────────────────
|
||||
|
||||
describe("_validate_token_with_provider", () => {
|
||||
it("should return 0 when no test function provided (empty string)", () => {
|
||||
const result = runBash(`
|
||||
_validate_token_with_provider "" MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 0 when test function succeeds", () => {
|
||||
const result = runBash(`
|
||||
test_success() { return 0; }
|
||||
export MY_TOKEN="valid-token"
|
||||
_validate_token_with_provider test_success MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 1 when test function fails", () => {
|
||||
const result = runBash(`
|
||||
test_fail() { return 1; }
|
||||
export MY_TOKEN="invalid-token"
|
||||
_validate_token_with_provider test_fail MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should unset the env var when validation fails", () => {
|
||||
const result = runBash(`
|
||||
test_fail() { return 1; }
|
||||
export MY_TOKEN="will-be-unset"
|
||||
_validate_token_with_provider test_fail MY_TOKEN "TestProvider" 2>/dev/null
|
||||
echo "MY_TOKEN=\${MY_TOKEN:-UNSET}"
|
||||
`);
|
||||
expect(result.stdout).toContain("MY_TOKEN=UNSET");
|
||||
});
|
||||
|
||||
it("should log authentication failed message on failure", () => {
|
||||
const result = runBash(`
|
||||
test_fail() { return 1; }
|
||||
export MY_TOKEN="bad"
|
||||
_validate_token_with_provider test_fail MY_TOKEN "Lambda" 2>&1
|
||||
`);
|
||||
expect(result.stdout).toContain("Authentication failed");
|
||||
expect(result.stdout).toContain("Lambda");
|
||||
});
|
||||
|
||||
it("should not unset env var when validation succeeds", () => {
|
||||
const result = runBash(`
|
||||
test_ok() { return 0; }
|
||||
export MY_TOKEN="good-token"
|
||||
_validate_token_with_provider test_ok MY_TOKEN "TestProvider"
|
||||
echo "MY_TOKEN=$MY_TOKEN"
|
||||
`);
|
||||
expect(result.stdout).toContain("MY_TOKEN=good-token");
|
||||
});
|
||||
});
|
||||
|
||||
// ── _save_token_to_config ─────────────────────────────────────────────────
|
||||
|
||||
describe("_save_token_to_config", () => {
|
||||
it("should create config file with api_key and token fields", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "subdir", "provider.json");
|
||||
|
||||
runBash(`_save_token_to_config "${configFile}" "my-secret-token" 2>/dev/null`);
|
||||
|
||||
expect(existsSync(configFile)).toBe(true);
|
||||
const content = readFileSync(configFile, "utf-8");
|
||||
const parsed = JSON.parse(content);
|
||||
expect(parsed.api_key).toBe("my-secret-token");
|
||||
expect(parsed.token).toBe("my-secret-token");
|
||||
});
|
||||
|
||||
it("should create parent directories if they do not exist", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "deep", "nested", "config.json");
|
||||
|
||||
runBash(`_save_token_to_config "${configFile}" "test-token" 2>/dev/null`);
|
||||
|
||||
expect(existsSync(configFile)).toBe(true);
|
||||
});
|
||||
|
||||
it("should set file permissions to 600", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "secure.json");
|
||||
|
||||
runBash(`_save_token_to_config "${configFile}" "secret" 2>/dev/null`);
|
||||
|
||||
const stats = statSync(configFile);
|
||||
const mode = (stats.mode & 0o777).toString(8);
|
||||
expect(mode).toBe("600");
|
||||
});
|
||||
|
||||
it("should properly JSON-escape tokens with special characters", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "special.json");
|
||||
|
||||
// Token with quotes and backslashes
|
||||
runBash(`_save_token_to_config "${configFile}" 'token-with-"quotes"' 2>/dev/null`);
|
||||
|
||||
const content = readFileSync(configFile, "utf-8");
|
||||
// Should be valid JSON
|
||||
expect(() => JSON.parse(content)).not.toThrow();
|
||||
const parsed = JSON.parse(content);
|
||||
expect(parsed.api_key).toBe('token-with-"quotes"');
|
||||
});
|
||||
|
||||
it("should overwrite existing config file", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "provider.json");
|
||||
writeFileSync(configFile, JSON.stringify({ api_key: "old-token" }));
|
||||
|
||||
runBash(`_save_token_to_config "${configFile}" "new-token" 2>/dev/null`);
|
||||
|
||||
const content = readFileSync(configFile, "utf-8");
|
||||
const parsed = JSON.parse(content);
|
||||
expect(parsed.api_key).toBe("new-token");
|
||||
});
|
||||
|
||||
it("should write valid JSON that can be re-read by _load_token_from_config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "roundtrip.json");
|
||||
|
||||
// Save token
|
||||
runBash(`_save_token_to_config "${configFile}" "roundtrip-value" 2>/dev/null`);
|
||||
|
||||
// Load it back
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" LOADED_TOKEN "Test" 2>/dev/null
|
||||
echo "$LOADED_TOKEN"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toBe("roundtrip-value");
|
||||
});
|
||||
|
||||
it("should handle empty token string", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "empty.json");
|
||||
|
||||
runBash(`_save_token_to_config "${configFile}" "" 2>/dev/null`);
|
||||
|
||||
expect(existsSync(configFile)).toBe(true);
|
||||
const content = readFileSync(configFile, "utf-8");
|
||||
expect(() => JSON.parse(content)).not.toThrow();
|
||||
const parsed = JSON.parse(content);
|
||||
expect(parsed.api_key).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
// ── _multi_creds_all_env_set ──────────────────────────────────────────────
|
||||
|
||||
describe("_multi_creds_all_env_set", () => {
|
||||
it("should return 0 when all env vars are set", () => {
|
||||
const result = runBash(`
|
||||
export VAR_A="a"
|
||||
export VAR_B="b"
|
||||
export VAR_C="c"
|
||||
_multi_creds_all_env_set VAR_A VAR_B VAR_C
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 1 when any env var is missing", () => {
|
||||
const result = runBash(`
|
||||
export VAR_A="a"
|
||||
unset VAR_B 2>/dev/null
|
||||
_multi_creds_all_env_set VAR_A VAR_B
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 when any env var is empty string", () => {
|
||||
const result = runBash(`
|
||||
export VAR_A="a"
|
||||
export VAR_B=""
|
||||
_multi_creds_all_env_set VAR_A VAR_B
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 0 with a single env var that is set", () => {
|
||||
const result = runBash(`
|
||||
export SINGLE_VAR="value"
|
||||
_multi_creds_all_env_set SINGLE_VAR
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 1 when first env var is missing but second is set", () => {
|
||||
const result = runBash(`
|
||||
unset FIRST_VAR 2>/dev/null
|
||||
export SECOND_VAR="present"
|
||||
_multi_creds_all_env_set FIRST_VAR SECOND_VAR
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 when last env var is missing", () => {
|
||||
const result = runBash(`
|
||||
export VAR_A="a"
|
||||
export VAR_B="b"
|
||||
unset VAR_C 2>/dev/null
|
||||
_multi_creds_all_env_set VAR_A VAR_B VAR_C
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 0 with no arguments (vacuously true)", () => {
|
||||
const result = runBash(`
|
||||
_multi_creds_all_env_set
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── _multi_creds_load_config ──────────────────────────────────────────────
|
||||
|
||||
describe("_multi_creds_load_config", () => {
|
||||
it("should load two credentials from JSON config into env vars", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "multi.json");
|
||||
writeFileSync(configFile, JSON.stringify({
|
||||
client_id: "my-client-id",
|
||||
client_secret: "my-secret",
|
||||
}));
|
||||
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "${configFile}" 2 CRED_ID CRED_SECRET client_id client_secret
|
||||
echo "ID=$CRED_ID"
|
||||
echo "SECRET=$CRED_SECRET"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("ID=my-client-id");
|
||||
expect(result.stdout).toContain("SECRET=my-secret");
|
||||
});
|
||||
|
||||
it("should return 1 when config file does not exist", () => {
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "/nonexistent/config.json" 1 MY_VAR my_key
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 when a field is empty in config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "partial.json");
|
||||
writeFileSync(configFile, JSON.stringify({
|
||||
client_id: "has-value",
|
||||
client_secret: "",
|
||||
}));
|
||||
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "${configFile}" 2 CRED_ID CRED_SECRET client_id client_secret
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should return 1 when a field is missing from config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "missing.json");
|
||||
writeFileSync(configFile, JSON.stringify({
|
||||
client_id: "has-value",
|
||||
}));
|
||||
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "${configFile}" 2 CRED_ID CRED_SECRET client_id client_secret
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should load a single credential from config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "single.json");
|
||||
writeFileSync(configFile, JSON.stringify({ api_key: "single-value" }));
|
||||
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "${configFile}" 1 MY_KEY api_key
|
||||
echo "KEY=$MY_KEY"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("KEY=single-value");
|
||||
});
|
||||
|
||||
it("should load three credentials from config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "three.json");
|
||||
writeFileSync(configFile, JSON.stringify({
|
||||
username: "user1",
|
||||
password: "pass1",
|
||||
project: "proj1",
|
||||
}));
|
||||
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "${configFile}" 3 MY_USER MY_PASS MY_PROJ username password project
|
||||
echo "USER=$MY_USER"
|
||||
echo "PASS=$MY_PASS"
|
||||
echo "PROJ=$MY_PROJ"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("USER=user1");
|
||||
expect(result.stdout).toContain("PASS=pass1");
|
||||
expect(result.stdout).toContain("PROJ=proj1");
|
||||
});
|
||||
|
||||
it("should return 1 for invalid JSON config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "invalid.json");
|
||||
writeFileSync(configFile, "not json {{{");
|
||||
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "${configFile}" 1 MY_VAR my_key
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ── _multi_creds_validate ─────────────────────────────────────────────────
|
||||
|
||||
describe("_multi_creds_validate", () => {
|
||||
it("should return 0 when no test function provided (empty string)", () => {
|
||||
const result = runBash(`
|
||||
_multi_creds_validate "" "TestProvider" VAR_A VAR_B
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 0 when test function succeeds", () => {
|
||||
const result = runBash(`
|
||||
test_ok() { return 0; }
|
||||
export VAR_A="a"
|
||||
_multi_creds_validate test_ok "TestProvider" VAR_A
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it("should return 1 when test function fails", () => {
|
||||
const result = runBash(`
|
||||
test_fail() { return 1; }
|
||||
export VAR_A="a"
|
||||
_multi_creds_validate test_fail "TestProvider" VAR_A 2>/dev/null
|
||||
`);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it("should unset all env vars when validation fails", () => {
|
||||
const result = runBash(`
|
||||
test_fail() { return 1; }
|
||||
export VAR_A="a"
|
||||
export VAR_B="b"
|
||||
_multi_creds_validate test_fail "TestProvider" VAR_A VAR_B 2>/dev/null
|
||||
echo "A=\${VAR_A:-UNSET}"
|
||||
echo "B=\${VAR_B:-UNSET}"
|
||||
`);
|
||||
expect(result.stdout).toContain("A=UNSET");
|
||||
expect(result.stdout).toContain("B=UNSET");
|
||||
});
|
||||
|
||||
it("should not unset env vars when validation succeeds", () => {
|
||||
const result = runBash(`
|
||||
test_ok() { return 0; }
|
||||
export VAR_A="kept"
|
||||
export VAR_B="also-kept"
|
||||
_multi_creds_validate test_ok "TestProvider" VAR_A VAR_B 2>/dev/null
|
||||
echo "A=$VAR_A"
|
||||
echo "B=$VAR_B"
|
||||
`);
|
||||
expect(result.stdout).toContain("A=kept");
|
||||
expect(result.stdout).toContain("B=also-kept");
|
||||
});
|
||||
|
||||
it("should log error message with provider name on failure", () => {
|
||||
const result = runBash(`
|
||||
test_fail() { return 1; }
|
||||
export VAR_A="a"
|
||||
_multi_creds_validate test_fail "Contabo" VAR_A 2>&1
|
||||
`);
|
||||
expect(result.stdout).toContain("Contabo");
|
||||
expect(result.stdout).toContain("Invalid");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Integration: _save_token_to_config + _load_token_from_config ──────────
|
||||
|
||||
describe("credential roundtrip integration", () => {
|
||||
it("should save and reload a simple token", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "roundtrip.json");
|
||||
|
||||
// Save
|
||||
runBash(`_save_token_to_config "${configFile}" "abc123" 2>/dev/null`);
|
||||
|
||||
// Load
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" LOADED "Test" 2>/dev/null
|
||||
echo "$LOADED"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toBe("abc123");
|
||||
});
|
||||
|
||||
it("should save and reload a token with special JSON characters", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "special.json");
|
||||
|
||||
// Save a token with backslash and newline-like content
|
||||
runBash(`_save_token_to_config "${configFile}" 'token\\with\\slashes' 2>/dev/null`);
|
||||
|
||||
// Load it back
|
||||
const result = runBash(`
|
||||
_load_token_from_config "${configFile}" LOADED "Test" 2>/dev/null
|
||||
echo "$LOADED"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("token");
|
||||
});
|
||||
|
||||
it("should validate after loading from config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "validated.json");
|
||||
|
||||
// Save
|
||||
runBash(`_save_token_to_config "${configFile}" "valid-token" 2>/dev/null`);
|
||||
|
||||
// Load and validate
|
||||
const result = runBash(`
|
||||
test_valid() { [[ "$MY_TOKEN" == "valid-token" ]]; }
|
||||
_load_token_from_config "${configFile}" MY_TOKEN "Test" 2>/dev/null
|
||||
_validate_token_with_provider test_valid MY_TOKEN "TestProvider"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Integration: multi-credential save and load ───────────────────────────
|
||||
|
||||
describe("multi-credential save and load integration", () => {
|
||||
it("should save with _save_json_config and load with _multi_creds_load_config", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "multi-roundtrip.json");
|
||||
|
||||
// Save two credentials
|
||||
runBash(`_save_json_config "${configFile}" client_id "my-id" client_secret "my-secret" 2>/dev/null`);
|
||||
|
||||
// Load them back
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "${configFile}" 2 LOADED_ID LOADED_SECRET client_id client_secret
|
||||
echo "ID=$LOADED_ID"
|
||||
echo "SECRET=$LOADED_SECRET"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("ID=my-id");
|
||||
expect(result.stdout).toContain("SECRET=my-secret");
|
||||
});
|
||||
|
||||
it("should save three credentials and load all three", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "three-creds.json");
|
||||
|
||||
runBash(`_save_json_config "${configFile}" username "user" password "pass" project_id "proj" 2>/dev/null`);
|
||||
|
||||
const result = runBash(`
|
||||
_multi_creds_load_config "${configFile}" 3 U P R username password project_id
|
||||
echo "U=$U P=$P R=$R"
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("U=user");
|
||||
expect(result.stdout).toContain("P=pass");
|
||||
expect(result.stdout).toContain("R=proj");
|
||||
});
|
||||
|
||||
it("should save config with chmod 600", () => {
|
||||
const dir = trackTempDir();
|
||||
const configFile = join(dir, "perms.json");
|
||||
|
||||
runBash(`_save_json_config "${configFile}" key "value" 2>/dev/null`);
|
||||
|
||||
const stats = statSync(configFile);
|
||||
const mode = (stats.mode & 0o777).toString(8);
|
||||
expect(mode).toBe("600");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue