213 行
5.8 KiB
JavaScript
可执行文件
213 行
5.8 KiB
JavaScript
可执行文件
#!/usr/bin/env node
|
|
const DEFAULT_BASE_URL = "https://oa.hyinda.com";
|
|
|
|
function parseArgs(argv) {
|
|
const options = {
|
|
baseUrl: process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL,
|
|
json: false,
|
|
strict: false,
|
|
};
|
|
|
|
for (const arg of argv) {
|
|
if (arg === "--json") {
|
|
options.json = true;
|
|
} else if (arg === "--strict") {
|
|
options.strict = true;
|
|
} else if (arg.startsWith("--base-url=")) {
|
|
options.baseUrl = arg.slice("--base-url=".length);
|
|
}
|
|
}
|
|
|
|
options.baseUrl = options.baseUrl.trim().replace(/\/$/, "");
|
|
return options;
|
|
}
|
|
|
|
function getCredential(role) {
|
|
const prefixByRole = {
|
|
finance: "LIVE_SMOKE_FINANCE",
|
|
employee: "LIVE_SMOKE_EMPLOYEE",
|
|
admin: "LIVE_SMOKE_ADMIN",
|
|
};
|
|
const prefix = prefixByRole[role];
|
|
const identifier = process.env[`${prefix}_IDENTIFIER`]?.trim();
|
|
const password = process.env[`${prefix}_PASSWORD`]?.trim();
|
|
|
|
if (!identifier || !password) {
|
|
return null;
|
|
}
|
|
|
|
return { identifier, password };
|
|
}
|
|
|
|
async function fetchJson(url, init = {}) {
|
|
const response = await fetch(url, {
|
|
...init,
|
|
signal: AbortSignal.timeout(15_000),
|
|
});
|
|
const text = await response.text();
|
|
let json = null;
|
|
try {
|
|
json = text ? JSON.parse(text) : null;
|
|
} catch {
|
|
json = null;
|
|
}
|
|
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
json,
|
|
};
|
|
}
|
|
|
|
function summarizeError(json) {
|
|
const error = json?.error ?? {};
|
|
const details = error.details ?? {};
|
|
return {
|
|
code: typeof error.code === "string" ? error.code : null,
|
|
featureFlag: typeof details.featureFlag === "string" ? details.featureFlag : null,
|
|
reason: typeof details.reason === "string" ? details.reason : null,
|
|
};
|
|
}
|
|
|
|
async function login(baseUrl, role, credential) {
|
|
const response = await fetchJson(`${baseUrl}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-device-fingerprint": `p2af-flag-off-${role}`,
|
|
},
|
|
body: JSON.stringify({
|
|
identifier: credential.identifier,
|
|
password: credential.password,
|
|
}),
|
|
});
|
|
|
|
const accessToken = response.json?.data?.accessToken;
|
|
if (!response.ok || typeof accessToken !== "string" || !accessToken) {
|
|
return {
|
|
ok: false,
|
|
role,
|
|
status: response.status,
|
|
error: summarizeError(response.json),
|
|
};
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
role,
|
|
accessToken,
|
|
};
|
|
}
|
|
|
|
async function probeEndpoint(baseUrl, role, accessToken, endpoint) {
|
|
const response = await fetchJson(`${baseUrl}${endpoint}`, {
|
|
method: "GET",
|
|
headers: {
|
|
authorization: `Bearer ${accessToken}`,
|
|
accept: "application/json",
|
|
},
|
|
});
|
|
const error = summarizeError(response.json);
|
|
const featureFlagDisabled = response.status === 409 && error.featureFlag === "payment_batch_v1_disabled";
|
|
const permissionDenied = response.status === 403 && error.code === "PERM_001";
|
|
const unexpectedOpen = response.status === 200;
|
|
const acceptable = featureFlagDisabled || (role !== "finance" && permissionDenied);
|
|
|
|
return {
|
|
role,
|
|
endpoint,
|
|
httpStatus: response.status,
|
|
status: acceptable ? "PASS" : "FAIL",
|
|
featureFlagDisabled,
|
|
permissionDenied,
|
|
unexpectedOpen,
|
|
error,
|
|
};
|
|
}
|
|
|
|
function buildRolePlan() {
|
|
return [
|
|
{ role: "finance", endpoints: ["/api/payment-batches", "/api/ap-operations"] },
|
|
{ role: "employee", endpoints: ["/api/payment-batches"] },
|
|
{ role: "admin", endpoints: ["/api/payment-batches"] },
|
|
];
|
|
}
|
|
|
|
function printHuman(summary) {
|
|
console.log(`[payment-batch-flag-off] status=${summary.status} base=${summary.baseUrl}`);
|
|
for (const missing of summary.missingCredentials) {
|
|
console.log(`[payment-batch-flag-off] ${missing.role} status=SKIP reason=missing_credentials`);
|
|
}
|
|
for (const loginFailure of summary.loginFailures) {
|
|
console.log(`[payment-batch-flag-off] ${loginFailure.role} status=FAIL loginStatus=${loginFailure.status}`);
|
|
}
|
|
for (const result of summary.results) {
|
|
const detail = result.featureFlagDisabled
|
|
? "featureFlag=payment_batch_v1_disabled"
|
|
: result.permissionDenied
|
|
? "permission=denied"
|
|
: `error=${result.error.code || "-"}`;
|
|
console.log(
|
|
`[payment-batch-flag-off] ${result.role} ${result.endpoint} status=${result.status} http=${result.httpStatus} ${detail}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
const missingCredentials = [];
|
|
const loginFailures = [];
|
|
const results = [];
|
|
|
|
for (const item of buildRolePlan()) {
|
|
const credential = getCredential(item.role);
|
|
if (!credential) {
|
|
missingCredentials.push({ role: item.role });
|
|
continue;
|
|
}
|
|
|
|
const session = await login(options.baseUrl, item.role, credential);
|
|
if (!session.ok) {
|
|
loginFailures.push(session);
|
|
continue;
|
|
}
|
|
|
|
for (const endpoint of item.endpoints) {
|
|
results.push(await probeEndpoint(options.baseUrl, item.role, session.accessToken, endpoint));
|
|
}
|
|
}
|
|
|
|
const hasFailures = loginFailures.length > 0 || results.some((result) => result.status !== "PASS");
|
|
const status = hasFailures ? "FAIL" : missingCredentials.length ? "SKIP" : results.length ? "PASS" : "SKIP";
|
|
const summary = {
|
|
checkedAt: new Date().toISOString(),
|
|
baseUrl: options.baseUrl,
|
|
status,
|
|
expectedFeatureFlag: "payment_batch_v1",
|
|
expectedState: "off",
|
|
readOnly: true,
|
|
missingCredentials,
|
|
loginFailures: loginFailures.map((item) => ({
|
|
role: item.role,
|
|
status: item.status,
|
|
error: item.error,
|
|
})),
|
|
results,
|
|
};
|
|
|
|
if (options.json) {
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
} else {
|
|
printHuman(summary);
|
|
}
|
|
|
|
if (status === "FAIL" || (options.strict && status !== "PASS")) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`[payment-batch-flag-off][ERROR] ${error instanceof Error ? error.message : String(error)}`);
|
|
process.exit(1);
|
|
});
|