Files
modelstudioai__cli/packages/commands/tests/e2e/auth.e2e.test.ts
T
2026-08-28 19:14:06 +08:00

655 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { describe, expect, test } from "vite-plus/test";
import {
isDashScopeE2EReady,
isOpenApiE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { AUTH_ROUTES } from "./topic-routes.ts";
/** Auth E2E本地参数/持久化契约默认执行;真实鉴权请求按对应 readiness gate 执行。 */
describe("e2e: auth", () => {
test("auth login --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "login", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/login|api-key/i);
expect(stderr).toMatch(/--console-site/);
expect(stderr).toMatch(/--open-api/);
});
test("auth login 一次只能选择一种登录模式", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--console",
"--api-key",
"sk-e2e-placeholder",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Choose exactly one login mode/);
});
test("auth login 模式专属参数不能脱离对应模式", async () => {
const openApiFlagOnly = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--access-key-id",
"LTAI-e2e",
]);
expect(openApiFlagOnly.exitCode).toBe(2);
expect(openApiFlagOnly.stderr).toMatch(/Use --open-api with --access-key-id/);
const baseUrlWithoutApiKey = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--console",
"--base-url",
"https://dashscope.aliyuncs.com",
]);
expect(baseUrlWithoutApiKey.exitCode).toBe(2);
expect(baseUrlWithoutApiKey.stderr).toMatch(/Use --base-url only with --api-key/);
const consoleSiteWithoutConsole = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--api-key",
"sk-e2e-placeholder",
"--console-site",
"international",
]);
expect(consoleSiteWithoutConsole.exitCode).toBe(2);
expect(consoleSiteWithoutConsole.stderr).toMatch(/Use --console-site only with --console/);
});
test("auth login --open-api 要求 AK/SK 成对输入", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--open-api",
"--access-key-id",
"LTAI-e2e",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Provide --access-key-id and --access-key-secret with --open-api/);
});
test("auth login --open-api --dry-run 使用 placeholder 时不请求服务端、不写配置", async () => {
const configDir = mkdtempSync(join(tmpdir(), "bl-auth-openapi-dry-run-"));
try {
const { stdout, stderr, exitCode } = await runCommandE2e(
AUTH_ROUTES,
[
"auth",
"login",
"--open-api",
"--access-key-id",
"LTAI-e2e-placeholder",
"--access-key-secret",
"secret-e2e-placeholder",
"--dry-run",
],
{
BAILIAN_CONFIG_DIR: configDir,
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
},
);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Would save OpenAPI AK/SK credentials");
expect(existsSync(join(configDir, "config.json"))).toBe(false);
} finally {
rmSync(configDir, { recursive: true, force: true });
}
});
test("auth logout --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "logout", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/logout|dry-run|yes/i);
});
test("auth status --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "status", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/status|output/i);
});
test("auth login 缺少 --api-key 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "login", "--quiet"]);
expect(exitCode, stderr).toBe(2);
expect(stderr).toMatch(/Choose exactly one login mode/);
});
test("auth login --dry-run --api-key 不发起落盘", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--dry-run",
"--api-key",
"sk-e2e-dry-run-placeholder",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Would save API key.");
});
test("auth login --dry-run 仍校验显式 Base URL", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--dry-run",
"--api-key",
"sk-e2e-dry-run-placeholder",
"--base-url",
"ftp://example.com/models",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid model base URL/);
});
test("auth login --api-key 原子保存凭证和 Base URL无联网探测", async () => {
const configDir = makeE2eOutputDir("auth-api-key-login");
const origin = "https://dashscope.example.test";
const sdkBaseUrl = `${origin}/compatible-mode/v1/?source=login#fragment`;
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--api-key", "sk-e2e-placeholder", "--base-url", sdkBaseUrl],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode, login.stderr).toBe(0);
expect(login.stderr).toMatch(/API key saved to/);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBe("sk-e2e-placeholder");
expect(config.base_url).toBe(origin);
});
test("auth login --config token-plan 接受 Anthropic SDK Base URL", async () => {
const configDir = makeE2eOutputDir("auth-token-plan-anthropic-base-url");
const origin = "https://token-plan.example.test";
const login = await runCommandE2e(
AUTH_ROUTES,
[
"auth",
"login",
"--config",
"token-plan",
"--api-key",
"sk-sp-e2e-placeholder",
"--base-url",
`${origin}/apps/anthropic?source=sdk#fragment`,
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode, login.stderr).toBe(0);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: origin,
default_text_model: "qwen3.8-max",
default_video_model: "happyhorse-1.1-t2v",
default_image_to_video_model: "happyhorse-1.1-i2v",
default_reference_to_video_model: "happyhorse-1.1-r2v",
default_image_model: "wan2.7-image",
default_speech_model: "qwen-audio-3.0-tts-plus",
default_speech_recognition_model: "qwen-audio-3.0-asr-flash",
api_key_capabilities: [
"text.chat",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
});
test("auth login --config token-plan 追加新版 capability preset 且不删除已有能力", async () => {
const configDir = makeE2eOutputDir("auth-token-plan-preset-login");
writeFileSync(
join(configDir, "config.json"),
JSON.stringify(
{
"token-plan": {
default_text_model: "custom-text-model",
default_video_model: "custom-video-model",
default_image_to_video_model: "custom-image-to-video-model",
default_reference_to_video_model: "custom-reference-to-video-model",
default_image_model: "custom-image-model",
api_key_capabilities: ["text.chat", "custom.command"],
},
},
null,
2,
) + "\n",
);
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--config", "token-plan", "--api-key", "sk-sp-e2e-placeholder"],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "sk-env-must-not-be-persisted",
DASHSCOPE_BASE_URL: "https://env-must-not-override-preset.example.test",
},
);
expect(login.exitCode, login.stderr).toBe(0);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBeUndefined();
expect(config.active_config).toBe("token-plan");
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
default_text_model: "qwen3.8-max",
default_video_model: "happyhorse-1.1-t2v",
default_image_to_video_model: "happyhorse-1.1-i2v",
default_reference_to_video_model: "happyhorse-1.1-r2v",
default_image_model: "wan2.7-image",
default_speech_model: "qwen-audio-3.0-tts-plus",
default_speech_recognition_model: "qwen-audio-3.0-asr-flash",
api_key_capabilities: [
"text.chat",
"custom.command",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
expect((config["token-plan"] as Record<string, unknown>).base_url).not.toBe(
"https://env-must-not-override-preset.example.test",
);
expect((config["token-plan"] as Record<string, unknown>).api_key).not.toBe(
"sk-env-must-not-be-persisted",
);
});
test("auth login --config token-plan 为显式空白名单追加 capability preset", async () => {
const configDir = makeE2eOutputDir("auth-token-plan-empty-capabilities-login");
writeFileSync(
join(configDir, "config.json"),
JSON.stringify(
{
"token-plan": {
api_key_capabilities: [],
},
},
null,
2,
) + "\n",
);
const login = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--config", "token-plan", "--api-key", "sk-sp-e2e-placeholder"],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "https://env-ignored-for-persist.example.test",
},
);
expect(login.exitCode, login.stderr).toBe(0);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config["token-plan"]).toMatchObject({
api_key_capabilities: [
"text.chat",
"vision.describe",
"image.generate",
"image.edit",
"speech.recognize",
"speech.synthesize",
"video.generate",
"video.ref",
"video.task.get",
"video.download",
],
});
});
test("auth login 未传 --config 时写当前激活 Config", async () => {
const configDir = makeE2eOutputDir("auth-active-profile-login");
const storedBaseUrl = "https://dev-profile.example.test";
writeFileSync(
join(configDir, "config.json"),
JSON.stringify(
{
active_config: "dev",
dev: { base_url: storedBaseUrl },
},
null,
2,
) + "\n",
);
const env = {
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
};
const activeLogin = await runCommandE2e(
AUTH_ROUTES,
["auth", "login", "--api-key", "sk-active-placeholder"],
env,
);
expect(activeLogin.exitCode, activeLogin.stderr).toBe(0);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config.api_key).toBeUndefined();
expect(config.active_config).toBe("dev");
expect(config.dev).toMatchObject({
api_key: "sk-active-placeholder",
base_url: storedBaseUrl,
});
});
test("auth login --api-key 非法 Base URL 失败时不留下半配置", async () => {
const configDir = makeE2eOutputDir("auth-api-key-login-failure");
const login = await runCommandE2e(
AUTH_ROUTES,
[
"auth",
"login",
"--config",
"failed-profile",
"--api-key",
"sk-invalid",
"--base-url",
"ftp://example.com/models",
],
{
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
},
);
expect(login.exitCode).not.toBe(0);
expect(login.stderr).toMatch(/Invalid model base URL/);
expect(existsSync(join(configDir, "config.json"))).toBe(false);
});
test("auth login --dry-run 覆盖全局参数 --output json --timeout", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--dry-run",
"--api-key",
"sk-e2e-dry-run-placeholder",
"--output",
"json",
"--timeout",
"120",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("Would save API key.");
});
test("auth login 缺少密钥且 --output json 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"login",
"--output",
"json",
]);
expect(exitCode).toBe(2);
const err = JSON.parse(stderr.trim()) as { error?: { code?: number; message?: string } };
expect(err.error?.code).toBe(2);
expect(err.error?.message).toMatch(/Choose exactly one login mode/);
});
test("auth logout --dry-run 不写入配置", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"logout",
"--dry-run",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("No changes made.");
expect(stderr).not.toContain("Cleared api_key");
});
test("auth logout --dry-run --quiet", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"logout",
"--dry-run",
"--quiet",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("No changes made.");
});
test("auth logout --dry-run --output json不清除密钥", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"logout",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toContain("No changes made.");
expect(stderr).not.toContain("Cleared api_key");
});
test("auth logout 清除当前 Config 的全部凭证和 Base URL保留普通配置", async () => {
const configDir = makeE2eOutputDir("auth-logout-all");
writeFileSync(
join(configDir, "config.json"),
JSON.stringify(
{
api_key: "sk-e2e-placeholder",
base_url: "https://model.example.com",
access_token: "console-token-placeholder",
access_key_id: "LTAI-e2e-placeholder",
access_key_secret: "secret-e2e-placeholder",
security_token: "sts-e2e-placeholder",
output: "json",
},
null,
2,
) + "\n",
);
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "logout"], {
BAILIAN_CONFIG_DIR: configDir,
});
expect(exitCode, stderr).toBe(0);
expect(stderr).toContain("api_key / base_url / access_token");
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
expect(config).toEqual({ output: "json" });
});
test.skipIf(!isDashScopeE2EReady())("auth status 文本输出", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"status",
"--output",
"text",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toMatch(
/Authentication Status|API key:|Console token:|DashScope API:|Console gateway:/,
);
});
test.skipIf(!isDashScopeE2EReady())("auth status --output json", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"status",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
authenticated?: boolean;
api_key?: { source?: string; masked?: string; base_url?: string };
}>(stdout);
expect(data.authenticated).toBe(true);
expect(data.api_key?.source).toBeDefined();
});
test.skipIf(!isDashScopeE2EReady())(
"auth status --output json --quiet(base_url 经 env 指定;凭证域 flag 对 status 不可见)",
async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(
AUTH_ROUTES,
["auth", "status", "--output", "json", "--quiet"],
{ DASHSCOPE_BASE_URL: "https://dashscope.aliyuncs.com" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ authenticated?: boolean; api_key?: unknown }>(stdout);
expect(data.authenticated).toBe(true);
expect(data.api_key).toBeDefined();
},
);
test("auth status 不接受凭证域覆盖 flag(--base-url 报 Unknown flag)", async () => {
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
"auth",
"status",
"--base-url",
"https://x.test",
]);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/Unknown flag.*--base-url/);
});
test("auth status 展示 env OpenAPI AK/SK 且不接受 OpenAPI flag 覆盖", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(
AUTH_ROUTES,
["auth", "status", "--output", "json"],
{
ALIBABA_CLOUD_ACCESS_KEY_ID: "LTAI-e2e-placeholder",
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "secret-e2e-placeholder",
},
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
authenticated?: boolean;
openapi?: { source?: string; access_key_id?: string; access_key_secret?: string };
}>(stdout);
expect(data.authenticated).toBe(true);
expect(data.openapi?.source).toBe("env");
expect(data.openapi?.access_key_id).not.toBe("LTAI-e2e-placeholder");
expect(data.openapi?.access_key_secret).not.toBe("secret-e2e-placeholder");
const denied = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--access-key-id", "ak"]);
expect(denied.exitCode).not.toBe(0);
expect(denied.stderr).toMatch(/Unknown flag.*--access-key-id/);
});
test.skipIf(!isOpenApiE2EReady())(
"auth login --open-api 使用环境中的真实 AK/SK持久化后支持单独 logout",
async () => {
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID!.trim();
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET!.trim();
const configDir = mkdtempSync(join(tmpdir(), "bl-auth-openapi-login-"));
const env = {
BAILIAN_CONFIG_DIR: configDir,
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
};
try {
const login = await runCommandE2e(
AUTH_ROUTES,
[
"auth",
"login",
"--open-api",
"--access-key-id",
accessKeyId,
"--access-key-secret",
accessKeySecret,
],
env,
);
expect(login.exitCode, login.stderr).toBe(0);
expect(login.stderr).toMatch(/OpenAPI credentials saved/);
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
string,
unknown
>;
// 只断言布尔结果,避免失败 diff 把真实凭证打印到测试日志。
expect(config.access_key_id === accessKeyId).toBe(true);
expect(config.access_key_secret === accessKeySecret).toBe(true);
expect(config.openapi_access_key_id).toBeUndefined();
expect(config.openapi_access_key_secret).toBeUndefined();
const status = await runCommandE2e(
AUTH_ROUTES,
["auth", "status", "--output", "json"],
env,
);
expect(status.exitCode, status.stderr).toBe(0);
const data = parseStdoutJson<{
authenticated?: boolean;
openapi?: { source?: string; access_key_id?: string; access_key_secret?: string };
}>(status.stdout);
expect(data.authenticated).toBe(true);
expect(data.openapi?.source).toBe("config");
expect(data.openapi?.access_key_id === accessKeyId).toBe(false);
expect(data.openapi?.access_key_secret === accessKeySecret).toBe(false);
const logout = await runCommandE2e(AUTH_ROUTES, ["auth", "logout", "--open-api"], env);
expect(logout.exitCode, logout.stderr).toBe(0);
expect(logout.stderr).toMatch(/Cleared access_key_id/);
const after = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--output", "json"], env);
expect(after.exitCode, after.stderr).toBe(0);
const afterData = parseStdoutJson<{ authenticated?: boolean; openapi?: unknown }>(
after.stdout,
);
expect(afterData.openapi).toBeUndefined();
} finally {
rmSync(configDir, { recursive: true, force: true });
}
},
);
});