#!/usr/bin/env node import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { randomInt } from "node:crypto"; const DEFAULT_BASE_URL = "https://oa.hyinda.com"; const DEFAULT_REPORT_ROOT = "artifacts/qa-flow"; const REQUIRED_APPROVAL = "HRWD_CONTACT_LIVE_APPROVED"; function parseArgs(argv) { const options = { baseUrl: process.env.LIVE_SMOKE_BASE_URL || DEFAULT_BASE_URL, issueId: process.env.QA_ISSUE_ID || "HCLT", reportDir: process.env.HRWD_CONTACT_LIVE_REPORT_DIR || "", }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg.startsWith("--base-url=")) { options.baseUrl = arg.slice("--base-url=".length); } else if (arg.startsWith("--report-dir=")) { options.reportDir = arg.slice("--report-dir=".length); } } options.baseUrl = options.baseUrl.trim().replace(/\/$/, ""); options.issueId = options.issueId.trim() || "HCLT"; if (!options.reportDir) { const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); options.reportDir = join(DEFAULT_REPORT_ROOT, `hrwd-contact-live-test-${stamp}`); } return options; } function getCredential(role) { const prefixByRole = { admin: "LIVE_SMOKE_ADMIN", employee: "LIVE_SMOKE_EMPLOYEE", }; 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(30_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 ?? {}; return { code: typeof error.code === "string" ? error.code : null, featureFlag: typeof error.details?.featureFlag === "string" ? error.details.featureFlag : null, reason: typeof error.details?.reason === "string" ? error.details.reason : null, }; } async function login(baseUrl, role, credential) { const result = await fetchJson(`${baseUrl}/api/auth/login`, { method: "POST", headers: { "content-type": "application/json", "x-device-fingerprint": `hrwd-contact-live-${role}`, }, body: JSON.stringify({ identifier: credential.identifier, password: credential.password, }), }); const accessToken = result.json?.data?.accessToken; if (!result.ok || typeof accessToken !== "string" || !accessToken) { throw new Error(`${role} login failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim()); } return accessToken; } async function callApi(baseUrl, token, method, pathName, body) { return fetchJson(`${baseUrl}${pathName}`, { method, headers: { authorization: `Bearer ${token}`, accept: "application/json", ...(body ? { "content-type": "application/json" } : {}), }, ...(body ? { body: JSON.stringify(body) } : {}), }); } function maskedId(value) { const raw = String(value ?? ""); if (!raw) return null; return raw.length <= 8 ? `${raw.slice(0, 2)}***` : `${raw.slice(0, 8)}***`; } function buildMarker() { const now = new Date(); const pad = (value) => String(value).padStart(2, "0"); return [ now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate()), pad(now.getHours()), pad(now.getMinutes()), pad(now.getSeconds()), ].join(""); } function randomTestPhone() { return `199${String(randomInt(0, 100_000_000)).padStart(8, "0")}`; } function buildTestPayload(marker, attempt) { const suffix = attempt === 0 ? marker : `${marker}R${attempt}`; return { employeeNo: `HRWDTEST-${suffix}`, name: `HRWDTEST-${suffix}`, phone: randomTestPhone(), email: `hrwdtest-${suffix.toLowerCase()}@hyinda.com`, title: "HRWDTEST-Onboarding", roles: ["employee"], syncWecomDirectory: false, }; } function syncRunSummary(run) { if (!run) { return null; } const responseSummary = run.responseSummary && typeof run.responseSummary === "object" ? run.responseSummary : {}; return { operation: run.operation ?? null, syncStatus: run.status ?? null, code: run.errorCode ?? null, errcode: responseSummary.errcode ?? null, externalUserIdPrefix: maskedId(run.externalUserId), }; } function assertWecomSyncSuccess(label, syncResult, expectedOperation) { const run = syncResult?.run ?? null; const errcode = run?.responseSummary?.errcode; if ( !syncResult || syncResult.status !== "success" || syncResult.code !== "wecom_contact_synced" || run?.operation !== expectedOperation || Number(errcode ?? 0) !== 0 ) { const summary = syncRunSummary(run); throw new Error(`${label} failed: ${JSON.stringify({ status: syncResult?.status ?? null, code: syncResult?.code ?? null, run: summary, })}`); } return syncRunSummary(run); } function passStep(name, details = {}) { return { name, ...details, status: "PASS" }; } function failStep(name, details = {}) { return { name, ...details, status: "FAIL" }; } async function createEmployeeWithRetry(baseUrl, adminToken, marker) { let lastError = null; for (let attempt = 0; attempt < 2; attempt += 1) { const payload = buildTestPayload(marker, attempt); const result = await callApi(baseUrl, adminToken, "POST", "/api/users", payload); if (result.ok) { const data = result.json?.data ?? {}; return { employee: data, oneTimePassword: data.oneTimePassword, marker: payload.name, }; } lastError = { httpStatus: result.status, error: summarizeError(result.json), }; if (result.status !== 409) { break; } } throw new Error(`employee create failed: ${JSON.stringify(lastError)}`); } function getPendingOffboardingTasks(profileLike) { const tasks = profileLike?.lifecycle?.checklistTasks ?? profileLike?.checklistTasks ?? []; return tasks.filter((task) => task.scene === "offboarding" && task.status === "pending"); } async function getDirectoryState(baseUrl, adminToken, employeeId) { const result = await callApi(baseUrl, adminToken, "GET", `/api/employees/${employeeId}/wecom-directory-sync`); if (!result.ok) { throw new Error(`directory state failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim()); } return result.json?.data ?? {}; } async function assertAuditAction(baseUrl, adminToken, action, targetId, dateFrom) { const params = new URLSearchParams({ action, targetType: "employee", page: "1", pageSize: "100", dateFrom, }); const result = await callApi(baseUrl, adminToken, "GET", `/api/audit-logs?${params.toString()}`); if (!result.ok) { throw new Error(`audit ${action} failed: HTTP ${result.status} ${summarizeError(result.json).code ?? ""}`.trim()); } const items = result.json?.data?.items ?? []; const matched = items.some((item) => item.targetId === targetId && item.action === action); if (!matched) { throw new Error(`audit ${action} not found for target`); } return { action, matched: true }; } async function assertEmployeeCannotAccessSync(baseUrl, employeeToken, targetEmployeeId) { const view = await callApi(baseUrl, employeeToken, "GET", `/api/employees/${targetEmployeeId}/wecom-directory-sync`); if (view.status !== 403) { throw new Error(`employee sync view should be forbidden, got HTTP ${view.status}`); } const retry = await callApi(baseUrl, employeeToken, "POST", `/api/employees/${targetEmployeeId}/wecom-directory-sync/retry`, { operation: "update", }); if (retry.status !== 403) { throw new Error(`employee sync retry should be forbidden, got HTTP ${retry.status}`); } } async function main() { const options = parseArgs(process.argv.slice(2)); if (process.env[REQUIRED_APPROVAL] !== "1") { throw new Error(`${REQUIRED_APPROVAL}=1 is required before public HRWDTEST writes`); } const adminCredential = getCredential("admin"); const employeeCredential = getCredential("employee"); if (!adminCredential || !employeeCredential) { throw new Error("missing LIVE_SMOKE_ADMIN_* or LIVE_SMOKE_EMPLOYEE_* credentials"); } mkdirSync(options.reportDir, { recursive: true }); const startedAt = new Date().toISOString(); const markerStamp = buildMarker(); const steps = []; let createdEmployeeId = null; let createdMarker = `HRWDTEST-${markerStamp}`; let finalStatus = "FAIL"; try { const adminToken = await login(options.baseUrl, "admin", adminCredential); steps.push(passStep("admin-login")); const employeeToken = await login(options.baseUrl, "employee", employeeCredential); steps.push(passStep("employee-login")); const created = await createEmployeeWithRetry(options.baseUrl, adminToken, markerStamp); const employee = created.employee; createdEmployeeId = employee.id; createdMarker = created.marker; const oneTimePassword = created.oneTimePassword; if (!createdEmployeeId || !oneTimePassword) { throw new Error("employee create response missing id or one-time password"); } steps.push(passStep("employee-created", { employeeIdPrefix: maskedId(createdEmployeeId), marker: createdMarker, wecomSyncSkippedAtCreate: employee.wecomDirectorySync == null, })); const onboarding = await callApi(options.baseUrl, adminToken, "POST", `/api/employees/${createdEmployeeId}/onboarding/start`, { effectiveDate: new Date().toISOString().slice(0, 10), notes: `${options.issueId} ${createdMarker}`, }); if (!onboarding.ok) { throw new Error(`onboarding failed: HTTP ${onboarding.status} ${summarizeError(onboarding.json).code ?? ""}`.trim()); } const createSync = assertWecomSyncSuccess("wecom create", onboarding.json?.data?.wecomDirectorySync, "create"); steps.push(passStep("wecom-create", createSync)); await assertEmployeeCannotAccessSync(options.baseUrl, employeeToken, createdEmployeeId); steps.push(passStep("employee-sync-permission-denied")); const updatedTitle = `HRWDTEST-Update-${markerStamp}`; const update = await callApi(options.baseUrl, adminToken, "PATCH", `/api/employees/${createdEmployeeId}/profile`, { title: updatedTitle, }); if (!update.ok) { throw new Error(`profile update failed: HTTP ${update.status} ${summarizeError(update.json).code ?? ""}`.trim()); } const updateSync = assertWecomSyncSuccess("wecom update", update.json?.data?.wecomDirectorySync, "update"); steps.push(passStep("wecom-update", updateSync)); const offboarding = await callApi(options.baseUrl, adminToken, "POST", `/api/employees/${createdEmployeeId}/offboarding/start`, { effectiveDate: new Date().toISOString().slice(0, 10), lastWorkingDate: new Date().toISOString().slice(0, 10), notes: `${options.issueId} ${createdMarker}`, }); if (!offboarding.ok) { throw new Error(`offboarding start failed: HTTP ${offboarding.status} ${summarizeError(offboarding.json).code ?? ""}`.trim()); } steps.push(passStep("offboarding-started")); let profile = offboarding.json?.data ?? {}; let tasks = getPendingOffboardingTasks(profile); if (!tasks.length) { const latestProfile = await callApi(options.baseUrl, adminToken, "GET", `/api/employees/${createdEmployeeId}/profile`); if (!latestProfile.ok) { throw new Error(`profile fetch failed: HTTP ${latestProfile.status} ${summarizeError(latestProfile.json).code ?? ""}`.trim()); } profile = latestProfile.json?.data ?? {}; tasks = getPendingOffboardingTasks(profile); } if (!tasks.length) { throw new Error("offboarding checklist has no pending tasks"); } let disableSync = null; for (const task of tasks) { const completed = await callApi(options.baseUrl, adminToken, "POST", `/api/employees/${createdEmployeeId}/checklist-tasks/${task.id}/complete`, {}); if (!completed.ok) { throw new Error(`checklist complete failed: HTTP ${completed.status} ${summarizeError(completed.json).code ?? ""}`.trim()); } if (completed.json?.data?.wecomDirectorySync) { disableSync = completed.json.data.wecomDirectorySync; } } const disableSummary = assertWecomSyncSuccess("wecom disable", disableSync, "disable"); steps.push(passStep("wecom-disable", { ...disableSummary, completedTaskCount: tasks.length, })); const finalProfile = await callApi(options.baseUrl, adminToken, "GET", `/api/employees/${createdEmployeeId}/profile`); if (!finalProfile.ok) { throw new Error(`final profile failed: HTTP ${finalProfile.status} ${summarizeError(finalProfile.json).code ?? ""}`.trim()); } const finalEmployee = finalProfile.json?.data?.employee ?? {}; if (finalEmployee.status !== "archived" || finalEmployee.employmentStatus !== "inactive") { throw new Error(`employee final state mismatch: ${JSON.stringify({ status: finalEmployee.status ?? null, employmentStatus: finalEmployee.employmentStatus ?? null, })}`); } steps.push(passStep("oa-employee-disabled", { employeeStatus: finalEmployee.status, employmentStatus: finalEmployee.employmentStatus, })); const deniedLogin = await fetchJson(`${options.baseUrl}/api/auth/login`, { method: "POST", headers: { "content-type": "application/json", "x-device-fingerprint": "hrwd-contact-live-disabled-employee", }, body: JSON.stringify({ identifier: employee.email, password: oneTimePassword, }), }); if (deniedLogin.ok || deniedLogin.json?.data?.accessToken) { throw new Error("disabled employee login unexpectedly succeeded"); } steps.push(passStep("disabled-employee-login-denied", { httpStatus: deniedLogin.status, code: summarizeError(deniedLogin.json).code, })); const directoryState = await getDirectoryState(options.baseUrl, adminToken, createdEmployeeId); const latestSync = directoryState.wecomDirectory?.latestSync ?? null; steps.push(passStep("admin-sync-state-readable", { itemCount: Array.isArray(directoryState.items) ? directoryState.items.length : null, latest: syncRunSummary(latestSync), })); const auditResults = []; for (const action of ["WECOM_DIRECTORY_CREATE", "WECOM_DIRECTORY_UPDATE", "WECOM_DIRECTORY_DISABLE"]) { auditResults.push(await assertAuditAction(options.baseUrl, adminToken, action, createdEmployeeId, startedAt)); } steps.push(passStep("audit-actions", { actions: auditResults.map((item) => item.action), })); finalStatus = "PASS"; } catch (error) { steps.push(failStep("hrwd-contact-live-test", { message: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500), })); } finally { const summary = { checkedAt: new Date().toISOString(), issueId: options.issueId, baseUrl: options.baseUrl, status: finalStatus, marker: createdMarker, employeeIdPrefix: maskedId(createdEmployeeId), finalPolicy: "keep_offboarded_and_wecom_disabled_do_not_delete", steps, }; writeFileSync(join(options.reportDir, "summary.json"), `${JSON.stringify(summary, null, 2)}\n`, "utf8"); console.log(`[hrwd-contact-live-test] status=${summary.status} base=${summary.baseUrl} marker=${summary.marker}`); for (const step of steps) { const detail = step.httpStatus ? ` http=${step.httpStatus}` : ""; const code = step.code ? ` code=${step.code}` : ""; console.log(`[hrwd-contact-live-test] ${step.name} status=${step.status}${detail}${code}`); } console.log(`[hrwd-contact-live-test] report=${options.reportDir}`); } if (finalStatus !== "PASS") { process.exit(1); } } main().catch((error) => { console.error(`[hrwd-contact-live-test][ERROR] ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });