test(parity): add enterprise browser smoke coverage
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = process.env.GOCHAT_ROOT || process.cwd();
|
||||
const chatwootDir = process.env.CHATWOOT_DIR || path.join(root, 'reference/chatwoot');
|
||||
const logDir = process.env.GOCHAT_SMOKE_LOG_DIR || path.join(root, '.tmp/frontend-smoke');
|
||||
const apiHost = process.env.GOCHAT_SMOKE_API_HOST || '127.0.0.1';
|
||||
const apiPort = process.env.GOCHAT_SMOKE_API_PORT || '3000';
|
||||
const frontendHost = process.env.GOCHAT_SMOKE_FRONTEND_HOST || '127.0.0.1';
|
||||
const frontendPort = process.env.GOCHAT_SMOKE_FRONTEND_PORT || '3036';
|
||||
const chromePath = process.env.GOCHAT_SMOKE_CHROME || '/usr/bin/google-chrome';
|
||||
const frontendBaseURL = `http://${frontendHost}:${frontendPort}`;
|
||||
const apiBaseURL = `http://${apiHost}:${apiPort}`;
|
||||
const enterpriseMode = process.argv.includes('--enterprise');
|
||||
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
|
||||
const seed = JSON.parse(readFileSync(path.join(logDir, 'seed.json'), 'utf8'));
|
||||
const report = {
|
||||
started_at: new Date().toISOString(),
|
||||
mode: enterpriseMode ? 'enterprise' : 'core',
|
||||
frontend_base_url: frontendBaseURL,
|
||||
api_base_url: apiBaseURL,
|
||||
account_id: seed.account_id,
|
||||
requests: [],
|
||||
console: [],
|
||||
checks: [],
|
||||
};
|
||||
|
||||
function smokeHTML(entrypoint, route) {
|
||||
const config = {
|
||||
apiHost: apiBaseURL,
|
||||
hostURL: frontendBaseURL,
|
||||
helpCenterURL: '',
|
||||
allowedLoginMethods: ['email'],
|
||||
signupEnabled: 'false',
|
||||
isEnterprise: 'true',
|
||||
isMfaEnabled: 'false',
|
||||
enabledLanguages: [{ iso_639_1_code: 'en', name: 'English' }],
|
||||
helpUrls: {},
|
||||
selectedLocale: 'en',
|
||||
};
|
||||
const globalConfig = {
|
||||
INSTALLATION_NAME: 'GoChat',
|
||||
BRAND_NAME: 'GoChat',
|
||||
LOGO: '/logo.png',
|
||||
LOGO_DARK: '',
|
||||
LOGO_THUMBNAIL: '/logo.png',
|
||||
IS_ENTERPRISE: 'true',
|
||||
DISABLE_USER_PROFILE_UPDATE: 'false',
|
||||
DIRECT_UPLOADS_ENABLED: 'false',
|
||||
MAXIMUM_FILE_UPLOAD_SIZE: '40',
|
||||
ACTIVE_PLATFORM_BANNERS: [],
|
||||
LOGOUT_REDIRECT_LINK: '/app/login',
|
||||
};
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GoChat Smoke</title>
|
||||
<script>
|
||||
history.replaceState({}, '', ${JSON.stringify(route)});
|
||||
window.chatwootConfig = ${JSON.stringify(config)};
|
||||
window.globalConfig = ${JSON.stringify(globalConfig)};
|
||||
window.browserConfig = { browser_name: 'chrome' };
|
||||
window.errorLoggingConfig = '';
|
||||
window.analyticsConfig = { token: '' };
|
||||
</script>
|
||||
<script type="module" src="/app/javascript/entrypoints/${entrypoint}.js"></script>
|
||||
</head>
|
||||
<body class="text-slate-600"><div id="app"></div></body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
const tmpHTMLDir = path.join(chatwootDir, 'tmp');
|
||||
mkdirSync(tmpHTMLDir, { recursive: true });
|
||||
|
||||
function writeSmokeShell(name, entrypoint, route) {
|
||||
writeFileSync(path.join(tmpHTMLDir, `${name}.html`), smokeHTML(entrypoint, route));
|
||||
return `${frontendBaseURL}/tmp/${name}.html`;
|
||||
}
|
||||
|
||||
const smokePages = {
|
||||
login: writeSmokeShell('gochat-smoke-login', 'v3app', '/app/login'),
|
||||
dashboard: writeSmokeShell('gochat-smoke-dashboard', 'dashboard', `/app/accounts/${seed.account_id}/dashboard`),
|
||||
};
|
||||
|
||||
const enterprisePages = [
|
||||
{
|
||||
label: 'SLA reports screen',
|
||||
name: 'gochat-smoke-enterprise-sla-reports',
|
||||
route: `/app/accounts/${seed.account_id}/reports/sla`,
|
||||
requests: ['/applied_slas', '/applied_slas/metrics'],
|
||||
},
|
||||
{
|
||||
label: 'CSAT reports screen',
|
||||
name: 'gochat-smoke-enterprise-csat-reports',
|
||||
route: `/app/accounts/${seed.account_id}/reports/csat`,
|
||||
requests: ['/csat_survey_responses', '/csat_survey_responses/metrics'],
|
||||
},
|
||||
{
|
||||
label: 'automation rules screen',
|
||||
name: 'gochat-smoke-enterprise-automation',
|
||||
route: `/app/accounts/${seed.account_id}/settings/automation/list`,
|
||||
requests: ['/automation_rules'],
|
||||
},
|
||||
{
|
||||
label: 'macros screen',
|
||||
name: 'gochat-smoke-enterprise-macros',
|
||||
route: `/app/accounts/${seed.account_id}/settings/macros`,
|
||||
requests: ['/macros'],
|
||||
},
|
||||
{
|
||||
label: 'audit logs screen',
|
||||
name: 'gochat-smoke-enterprise-audit-logs',
|
||||
route: `/app/accounts/${seed.account_id}/settings/audit-logs/list`,
|
||||
requests: ['/audit_logs'],
|
||||
},
|
||||
{
|
||||
label: 'custom roles screen',
|
||||
name: 'gochat-smoke-enterprise-custom-roles',
|
||||
route: `/app/accounts/${seed.account_id}/settings/custom-roles/list`,
|
||||
requests: ['/custom_roles'],
|
||||
},
|
||||
{
|
||||
label: 'agent capacity screen',
|
||||
name: 'gochat-smoke-enterprise-agent-capacity',
|
||||
route: `/app/accounts/${seed.account_id}/settings/assignment-policy/capacity`,
|
||||
requests: ['/agent_capacity_policies'],
|
||||
},
|
||||
{
|
||||
label: 'agent capacity edit screen',
|
||||
name: 'gochat-smoke-enterprise-agent-capacity-edit',
|
||||
route: `/app/accounts/${seed.account_id}/settings/assignment-policy/capacity/edit/${seed.capacity_policy_id}`,
|
||||
requests: [`/agent_capacity_policies/${seed.capacity_policy_id}`, `/agent_capacity_policies/${seed.capacity_policy_id}/users`],
|
||||
},
|
||||
{
|
||||
label: 'Captain settings screen',
|
||||
name: 'gochat-smoke-enterprise-captain-settings',
|
||||
route: `/app/accounts/${seed.account_id}/settings/captain`,
|
||||
requests: ['/captain/preferences'],
|
||||
},
|
||||
{
|
||||
label: 'Captain assistants screen',
|
||||
name: 'gochat-smoke-enterprise-captain-assistants',
|
||||
route: `/app/accounts/${seed.account_id}/captain/captain_assistants_responses_index`,
|
||||
requests: ['/captain/assistants'],
|
||||
},
|
||||
].map(page => ({
|
||||
...page,
|
||||
url: writeSmokeShell(page.name, 'dashboard', page.route),
|
||||
}));
|
||||
|
||||
class CDPPage {
|
||||
constructor(ws, chrome) {
|
||||
this.ws = ws;
|
||||
this.chrome = chrome;
|
||||
this.nextID = 1;
|
||||
this.pending = new Map();
|
||||
this.listeners = new Map();
|
||||
ws.onmessage = event => this.handleMessage(JSON.parse(event.data));
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
if (message.id && this.pending.has(message.id)) {
|
||||
const { resolve, reject } = this.pending.get(message.id);
|
||||
this.pending.delete(message.id);
|
||||
if (message.error) reject(new Error(message.error.message));
|
||||
else resolve(message.result || {});
|
||||
return;
|
||||
}
|
||||
const handlers = this.listeners.get(message.method) || [];
|
||||
handlers.forEach(handler => handler(message.params || {}));
|
||||
}
|
||||
|
||||
send(method, params = {}) {
|
||||
const id = this.nextID++;
|
||||
this.ws.send(JSON.stringify({ id, method, params }));
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject });
|
||||
setTimeout(() => {
|
||||
if (this.pending.has(id)) {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`CDP command timed out: ${method}`));
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
on(method, handler) {
|
||||
const handlers = this.listeners.get(method) || [];
|
||||
handlers.push(handler);
|
||||
this.listeners.set(method, handlers);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.send('Page.enable');
|
||||
await this.send('Runtime.enable');
|
||||
await this.send('Network.enable');
|
||||
this.on('Runtime.consoleAPICalled', params => {
|
||||
report.console.push({
|
||||
type: params.type,
|
||||
text: (params.args || []).map(arg => arg.value || arg.description || '').join(' '),
|
||||
});
|
||||
});
|
||||
this.on('Runtime.exceptionThrown', params => {
|
||||
report.console.push({ type: 'exception', text: params.exceptionDetails?.text || 'exception' });
|
||||
});
|
||||
this.on('Network.responseReceived', params => {
|
||||
report.requests.push({
|
||||
url: params.response.url,
|
||||
status: params.response.status,
|
||||
type: params.type,
|
||||
});
|
||||
});
|
||||
this.on('Network.loadingFailed', params => {
|
||||
report.requests.push({ url: params.requestId, status: 0, errorText: params.errorText });
|
||||
});
|
||||
}
|
||||
|
||||
async navigate(url) {
|
||||
const loaded = new Promise(resolve => this.on('Page.loadEventFired', resolve));
|
||||
await this.send('Page.navigate', { url });
|
||||
await loaded;
|
||||
}
|
||||
|
||||
async eval(expression) {
|
||||
const result = await this.send('Runtime.evaluate', {
|
||||
expression,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
});
|
||||
if (result.exceptionDetails) {
|
||||
throw new Error(result.exceptionDetails.text || 'Runtime.evaluate failed');
|
||||
}
|
||||
return result.result?.value;
|
||||
}
|
||||
|
||||
async waitFor(expression, label, timeout = 30000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
if (await this.eval(expression)) {
|
||||
report.checks.push({ label, status: 'passed' });
|
||||
return;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${label}`);
|
||||
}
|
||||
|
||||
waitForRequest(substring, label, timeout = 30000) {
|
||||
return this.waitFor(
|
||||
`performance.getEntriesByType('resource').some(entry => entry.name.includes(${JSON.stringify(substring)}))`,
|
||||
label,
|
||||
timeout
|
||||
);
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.ws.close();
|
||||
this.chrome.kill('SIGTERM');
|
||||
}
|
||||
}
|
||||
|
||||
async function launchChrome() {
|
||||
const userDataDir = mkdtempSync(path.join(tmpdir(), 'gochat-chrome-'));
|
||||
const chrome = spawn(chromePath, [
|
||||
'--headless=new',
|
||||
'--disable-gpu',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-dev-shm-usage',
|
||||
'--remote-debugging-port=0',
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
'about:blank',
|
||||
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
const portFile = path.join(userDataDir, 'DevToolsActivePort');
|
||||
for (let i = 0; i < 80; i += 1) {
|
||||
try {
|
||||
const [port] = readFileSync(portFile, 'utf8').trim().split('\n');
|
||||
const target = await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: 'PUT' }).then(r => r.json());
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onopen = resolve;
|
||||
ws.onerror = reject;
|
||||
});
|
||||
const page = new CDPPage(ws, chrome);
|
||||
await page.init();
|
||||
return page;
|
||||
} catch {
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
}
|
||||
}
|
||||
chrome.kill('SIGTERM');
|
||||
throw new Error('Chrome DevTools did not become ready');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const page = await launchChrome();
|
||||
try {
|
||||
await page.navigate(smokePages.login);
|
||||
await page.waitFor('!!document.querySelector("input[name=email_address]")', 'login email input visible');
|
||||
await page.eval(`(() => {
|
||||
const email = document.querySelector('input[name=email_address]');
|
||||
const password = document.querySelector('input[name=password]');
|
||||
email.value = ${JSON.stringify(seed.admin_email)};
|
||||
password.value = ${JSON.stringify(seed.admin_password)};
|
||||
email.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
password.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
document.querySelector('[data-testid=submit_button], button[type=submit]').click();
|
||||
return true;
|
||||
})()`);
|
||||
await page.waitFor('document.cookie.includes("cw_d_session_info")', 'login stores Chatwoot auth cookie');
|
||||
await page.waitFor(
|
||||
`performance.getEntriesByType('resource').some(entry => entry.name.includes('/auth/sign_in'))`,
|
||||
'login calls auth/sign_in'
|
||||
);
|
||||
|
||||
await page.navigate(smokePages.dashboard);
|
||||
await page.waitFor('document.querySelector("#app") && document.querySelector("#app").children.length > 0', 'dashboard app mounted');
|
||||
await page.waitForRequest('/auth/validate_token', 'dashboard validates auth token');
|
||||
await page.waitForRequest(`/api/v1/accounts/${seed.account_id}/conversations`, 'dashboard requests conversations');
|
||||
|
||||
if (enterpriseMode) {
|
||||
for (const enterprisePage of enterprisePages) {
|
||||
await page.navigate(enterprisePage.url);
|
||||
await page.waitFor(
|
||||
'document.querySelector("#app") && document.querySelector("#app").children.length > 0',
|
||||
`${enterprisePage.label} app mounted`
|
||||
);
|
||||
for (const request of enterprisePage.requests) {
|
||||
await page.waitForRequest(request, `${enterprisePage.label} requests ${request}`);
|
||||
}
|
||||
}
|
||||
await page.eval(`fetch(${JSON.stringify(`${apiBaseURL}/api/v1/accounts/${seed.account_id}/captain/copilot_threads`)}, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ message: 'B12 enterprise browser copilot smoke', assistant_id: ${Number(seed.captain_assistant_id)}, conversation_id: ${Number(seed.conversation_id)} })
|
||||
}).then(response => response.ok)`);
|
||||
await page.waitForRequest('/captain/copilot_threads', 'browser context requests Copilot threads');
|
||||
}
|
||||
report.finished_at = new Date().toISOString();
|
||||
report.status = 'passed';
|
||||
} catch (error) {
|
||||
report.finished_at = new Date().toISOString();
|
||||
report.status = 'failed';
|
||||
report.error = error.message;
|
||||
throw error;
|
||||
} finally {
|
||||
writeFileSync(path.join(logDir, 'browser-smoke-report.json'), JSON.stringify(report, null, 2));
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -19,13 +19,16 @@ KEEP_ALIVE="true"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: scripts/parity_frontend_smoke.sh [--check|--print|--boot-only|--api-smoke] [--no-seed]
|
||||
Usage: scripts/parity_frontend_smoke.sh [--check|--print|--boot-only|--api-smoke|--browser-smoke|--enterprise-smoke|--enterprise-browser-smoke] [--no-seed]
|
||||
|
||||
Modes:
|
||||
--check Validate local prerequisites and write a readiness report.
|
||||
--print Print the exact boot commands without starting processes.
|
||||
--boot-only Start GoChat and Vite, verify /health and frontend HTTP, then exit.
|
||||
--api-smoke Run seed plus Chatwoot-frontend API assertions against a running GoChat backend.
|
||||
--browser-smoke Run API smoke, then drive the reused Chatwoot frontend in headless Chrome.
|
||||
--enterprise-smoke Run API smoke plus enterprise feature API assertions against GoChat.
|
||||
--enterprise-browser-smoke Run enterprise smoke, then navigate reused Chatwoot enterprise screens in headless Chrome.
|
||||
--no-seed Skip seed during --api-smoke and use GOCHAT_SEED_* values already present in DB.
|
||||
|
||||
Environment:
|
||||
@@ -37,6 +40,7 @@ Environment:
|
||||
GOCHAT_SMOKE_SEARCH_ENGINE Search engine for boot smoke. Default: meilisearch
|
||||
GOCHAT_SMOKE_MEILI_HOST Meilisearch URL. Default: http://127.0.0.1:7700
|
||||
GOCHAT_SMOKE_MEILI_API_KEY Meilisearch API key. Default: gochat_dev
|
||||
GOCHAT_SMOKE_CHROME Chrome binary for browser smoke. Default: /usr/bin/google-chrome
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -48,6 +52,9 @@ for arg in "$@"; do
|
||||
--print) MODE="print" ;;
|
||||
--boot-only) KEEP_ALIVE="false" ;;
|
||||
--api-smoke) MODE="api-smoke" ;;
|
||||
--browser-smoke) MODE="browser-smoke" ;;
|
||||
--enterprise-smoke) MODE="enterprise-smoke" ;;
|
||||
--enterprise-browser-smoke) MODE="enterprise-browser-smoke" ;;
|
||||
--no-seed) RUN_SEED="false" ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $arg" >&2; usage; exit 2 ;;
|
||||
@@ -89,6 +96,24 @@ scripts/parity_frontend_smoke.sh --boot-only
|
||||
scripts/parity_frontend_smoke.sh --api-smoke
|
||||
\`\`\`
|
||||
|
||||
## Browser Smoke Command
|
||||
|
||||
\`\`\`bash
|
||||
scripts/parity_frontend_smoke.sh --browser-smoke
|
||||
\`\`\`
|
||||
|
||||
## Enterprise Smoke Command
|
||||
|
||||
\`\`\`bash
|
||||
scripts/parity_frontend_smoke.sh --enterprise-smoke
|
||||
\`\`\`
|
||||
|
||||
## Enterprise Browser Smoke Command
|
||||
|
||||
\`\`\`bash
|
||||
scripts/parity_frontend_smoke.sh --enterprise-browser-smoke
|
||||
\`\`\`
|
||||
|
||||
## Backend
|
||||
|
||||
- URL: http://$API_HOST:$API_PORT
|
||||
@@ -167,6 +192,9 @@ print_commands() {
|
||||
echo "Backend: ${backend_cmd[*]}"
|
||||
echo "Frontend: (cd $CHATWOOT_DIR && ${frontend_cmd[*]})"
|
||||
echo "API smoke: scripts/parity_frontend_smoke.sh --api-smoke"
|
||||
echo "Browser smoke: scripts/parity_frontend_smoke.sh --browser-smoke"
|
||||
echo "Enterprise smoke: scripts/parity_frontend_smoke.sh --enterprise-smoke"
|
||||
echo "Enterprise browser smoke: scripts/parity_frontend_smoke.sh --enterprise-browser-smoke"
|
||||
echo "Report: $REPORT_PATH"
|
||||
}
|
||||
|
||||
@@ -216,7 +244,7 @@ run_api_smoke() {
|
||||
(cd "$ROOT" && env GOCACHE="${GOCACHE:-/tmp/gochat-gocache}" GOMODCACHE="${GOMODCACHE:-/tmp/gochat-gomodcache}" go run ./cmd/gochat seed) >"$seed_file"
|
||||
else
|
||||
cat >"$seed_file" <<SEED
|
||||
{"admin_email":"${GOCHAT_SEED_ADMIN_EMAIL:-admin@gochat.local}","admin_password":"${GOCHAT_SEED_ADMIN_PASSWORD:-changeme}","account_id":"${GOCHAT_SMOKE_ACCOUNT_ID:-1}","inbox_id":"${GOCHAT_SMOKE_INBOX_ID:-1}","contact_id":"${GOCHAT_SMOKE_CONTACT_ID:-1}","company_id":"${GOCHAT_SMOKE_COMPANY_ID:-1}","conversation_display_id":"${GOCHAT_SMOKE_CONVERSATION_DISPLAY_ID:-1}","conversation_uuid":"${GOCHAT_SMOKE_CONVERSATION_UUID:-}"}
|
||||
{"admin_email":"${GOCHAT_SEED_ADMIN_EMAIL:-admin@gochat.local}","admin_password":"${GOCHAT_SEED_ADMIN_PASSWORD:-changeme}","account_id":"${GOCHAT_SMOKE_ACCOUNT_ID:-1}","inbox_id":"${GOCHAT_SMOKE_INBOX_ID:-1}","contact_id":"${GOCHAT_SMOKE_CONTACT_ID:-1}","company_id":"${GOCHAT_SMOKE_COMPANY_ID:-1}","conversation_id":"${GOCHAT_SMOKE_CONVERSATION_ID:-1}","conversation_display_id":"${GOCHAT_SMOKE_CONVERSATION_DISPLAY_ID:-1}","conversation_uuid":"${GOCHAT_SMOKE_CONVERSATION_UUID:-}","sla_policy_id":"${GOCHAT_SMOKE_SLA_POLICY_ID:-1}","custom_role_id":"${GOCHAT_SMOKE_CUSTOM_ROLE_ID:-1}","capacity_policy_id":"${GOCHAT_SMOKE_CAPACITY_POLICY_ID:-1}","captain_assistant_id":"${GOCHAT_SMOKE_CAPTAIN_ASSISTANT_ID:-1}"}
|
||||
SEED
|
||||
fi
|
||||
cp "$seed_file" "$LOG_DIR/seed.json"
|
||||
@@ -326,6 +354,201 @@ SEED
|
||||
echo "api smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
run_browser_smoke() {
|
||||
run_api_smoke
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT" "Chatwoot Vite"
|
||||
GOCHAT_ROOT="$ROOT" \
|
||||
CHATWOOT_DIR="$CHATWOOT_DIR" \
|
||||
GOCHAT_SMOKE_LOG_DIR="$LOG_DIR" \
|
||||
GOCHAT_SMOKE_API_HOST="$API_HOST" \
|
||||
GOCHAT_SMOKE_API_PORT="$API_PORT" \
|
||||
GOCHAT_SMOKE_FRONTEND_HOST="$FRONTEND_HOST" \
|
||||
GOCHAT_SMOKE_FRONTEND_PORT="$FRONTEND_PORT" \
|
||||
node "$ROOT/scripts/parity_frontend_browser_smoke.mjs"
|
||||
BOOT_BACKEND_RESULT="Passed /health during browser smoke" \
|
||||
BOOT_FRONTEND_RESULT="Passed Vite readiness during browser smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed browser login plus API smoke" \
|
||||
INBOX_RESULT="Passed API smoke" \
|
||||
CONVERSATION_RESULT="Passed dashboard browser request plus API smoke" \
|
||||
CRM_RESULT="Passed contact/company API smoke" \
|
||||
WIDGET_RESULT="Passed API smoke" \
|
||||
CSAT_RESULT="Passed public show API smoke" \
|
||||
ENTERPRISE_RESULT="Pending B12.3 browser/API smoke" \
|
||||
write_report "Browser smoke passed for reused Chatwoot login and dashboard boot; API smoke passed for core frontend paths." "Command run: \`scripts/parity_frontend_smoke.sh --browser-smoke\`. Browser report: \`$LOG_DIR/browser-smoke-report.json\`. B12.3 must add enterprise screen assertions."
|
||||
echo "browser smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
run_enterprise_api_smoke() {
|
||||
run_api_smoke
|
||||
|
||||
local seed_file account_id inbox_id conversation_id conversation_display_id conversation_uuid custom_role_id capacity_policy_id captain_assistant_id sla_policy_id
|
||||
seed_file="$LOG_DIR/seed.json"
|
||||
account_id="$(json_value "$seed_file" 'data.account_id')"
|
||||
inbox_id="$(json_value "$seed_file" 'data.inbox_id')"
|
||||
conversation_id="$(json_value "$seed_file" 'data.conversation_id')"
|
||||
conversation_display_id="$(json_value "$seed_file" 'data.conversation_display_id || 1')"
|
||||
conversation_uuid="$(json_value "$seed_file" 'data.conversation_uuid || ""')"
|
||||
custom_role_id="$(json_value "$seed_file" 'data.custom_role_id')"
|
||||
capacity_policy_id="$(json_value "$seed_file" 'data.capacity_policy_id')"
|
||||
captain_assistant_id="$(json_value "$seed_file" 'data.captain_assistant_id')"
|
||||
sla_policy_id="$(json_value "$seed_file" 'data.sla_policy_id')"
|
||||
|
||||
local body request_body created_id csv_file
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/applied_slas?page=1&sla_policy_id=$sla_policy_id"
|
||||
cp "$body" "$LOG_DIR/enterprise_applied_slas.json"
|
||||
json_assert "$body" 'Array.isArray(data.payload) && data.meta && Number(data.meta.current_page) === 1' "SLA report list returns Chatwoot payload/meta"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/applied_slas/metrics?sla_policy_id=$sla_policy_id"
|
||||
cp "$body" "$LOG_DIR/enterprise_applied_sla_metrics.json"
|
||||
json_assert "$body" 'typeof data === "object" && data !== null' "SLA report metrics returns JSON"
|
||||
|
||||
csv_file="$(tmp_file)"
|
||||
authed_curl -o "$csv_file" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/applied_slas/download?sla_policy_id=$sla_policy_id"
|
||||
cp "$csv_file" "$LOG_DIR/enterprise_applied_sla_download.csv"
|
||||
if ! grep -q "Conversation ID,SLA policy breached" "$csv_file"; then
|
||||
echo "assertion failed: SLA download returns Chatwoot CSV headers" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "ok: SLA download returns Chatwoot CSV headers"
|
||||
|
||||
if [[ -n "$conversation_uuid" ]]; then
|
||||
request_body="$(tmp_file)"
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ message: { submitted_values: [{ csat_survey_response: { rating: 5, feedback_message: "B12 enterprise smoke" } }] } }));' "$request_body"
|
||||
body="$(tmp_file)"
|
||||
curl -fsS -H "Content-Type: application/json" -X PATCH --data-binary "@$request_body" -o "$body" "http://$API_HOST:$API_PORT/public/api/v1/csat_survey/$conversation_uuid"
|
||||
cp "$body" "$LOG_DIR/enterprise_public_csat_submit.json"
|
||||
json_assert "$body" 'data.rating === 5 || (data.csat_survey_response && data.csat_survey_response.rating === 5)' "public CSAT submit creates account report data"
|
||||
fi
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/csat_survey_responses?rating=5"
|
||||
cp "$body" "$LOG_DIR/enterprise_csat_responses.json"
|
||||
json_assert "$body" 'Array.isArray(data)' "CSAT report list returns raw array"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/csat_survey_responses/metrics"
|
||||
cp "$body" "$LOG_DIR/enterprise_csat_metrics.json"
|
||||
json_assert "$body" 'Object.prototype.hasOwnProperty.call(data, "total_count") && Object.prototype.hasOwnProperty.call(data, "ratings_count")' "CSAT metrics returns report counters"
|
||||
|
||||
csv_file="$(tmp_file)"
|
||||
authed_curl -o "$csv_file" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/csat_survey_responses/download"
|
||||
cp "$csv_file" "$LOG_DIR/enterprise_csat_download.csv"
|
||||
if ! grep -q "Conversation ID" "$csv_file"; then
|
||||
echo "assertion failed: CSAT download returns CSV headers" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "ok: CSAT download returns CSV headers"
|
||||
|
||||
request_body="$(tmp_file)"
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ name: "B12 Enterprise Smoke Automation", description: "B12.3 smoke", event_name: "conversation_created", active: true, conditions: [], actions: [] }));' "$request_body"
|
||||
body="$(tmp_file)"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/automation_rules"
|
||||
cp "$body" "$LOG_DIR/enterprise_automation_create.json"
|
||||
json_assert "$body" 'data.name === "B12 Enterprise Smoke Automation" && data.event_name === "conversation_created"' "automation rule create returns raw Chatwoot rule"
|
||||
created_id="$(json_value "$body" 'data.id')"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/automation_rules"
|
||||
cp "$body" "$LOG_DIR/enterprise_automation_list.json"
|
||||
json_assert "$body" 'data.payload && data.payload.some(rule => Number(rule.id) === Number("'$created_id'"))' "automation rule list returns created rule in payload"
|
||||
|
||||
request_body="$(tmp_file)"
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ name: "B12 Enterprise Smoke Macro", visibility: "global", actions: [] }));' "$request_body"
|
||||
body="$(tmp_file)"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/macros"
|
||||
cp "$body" "$LOG_DIR/enterprise_macro_create.json"
|
||||
json_assert "$body" 'data.payload && data.payload.name === "B12 Enterprise Smoke Macro" && data.payload.visibility === "global"' "macro create returns Chatwoot payload"
|
||||
created_id="$(json_value "$body" 'data.payload.id')"
|
||||
|
||||
request_body="$(tmp_file)"
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ conversation_ids: [Number(process.argv[2])] }));' "$request_body" "$conversation_display_id"
|
||||
body="$(tmp_file)"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/macros/$created_id/execute"
|
||||
cp "$body" "$LOG_DIR/enterprise_macro_execute.txt"
|
||||
echo "ok: macro execute returns success"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/custom_roles"
|
||||
cp "$body" "$LOG_DIR/enterprise_custom_roles.json"
|
||||
json_assert "$body" 'Array.isArray(data) && data.some(role => Number(role.id) === Number("'$custom_role_id'") && Array.isArray(role.permissions))' "custom role list returns seeded enterprise role"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/agent_capacity_policies"
|
||||
cp "$body" "$LOG_DIR/enterprise_capacity_policies.json"
|
||||
json_assert "$body" 'Array.isArray(data) && data.some(policy => Number(policy.id) === Number("'$capacity_policy_id'"))' "agent capacity list returns seeded policy"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/agent_capacity_policies/$capacity_policy_id/users"
|
||||
cp "$body" "$LOG_DIR/enterprise_capacity_users.json"
|
||||
json_assert "$body" 'Array.isArray(data)' "agent capacity users returns array"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/audit_logs"
|
||||
cp "$body" "$LOG_DIR/enterprise_audit_logs.json"
|
||||
json_assert "$body" 'data.audit_logs && Array.isArray(data.audit_logs) && data.audit_logs.length >= 1 && data.current_page === 1' "audit logs return enterprise Jbuilder list"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/captain/preferences"
|
||||
cp "$body" "$LOG_DIR/enterprise_captain_preferences.json"
|
||||
json_assert "$body" 'data.providers && data.models && data.features' "Captain preferences returns providers/models/features"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/captain/assistants"
|
||||
cp "$body" "$LOG_DIR/enterprise_captain_assistants.json"
|
||||
json_assert "$body" 'data.payload && data.payload.some(assistant => Number(assistant.id) === Number("'$captain_assistant_id'"))' "Captain assistants list returns seeded assistant"
|
||||
|
||||
request_body="$(tmp_file)"
|
||||
node -e 'const fs = require("fs"); fs.writeFileSync(process.argv[1], JSON.stringify({ message: "B12 enterprise copilot smoke", assistant_id: Number(process.argv[2]), conversation_id: Number(process.argv[3]) }));' "$request_body" "$captain_assistant_id" "$conversation_id"
|
||||
body="$(tmp_file)"
|
||||
authed_curl -X POST --data-binary "@$request_body" -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/captain/copilot_threads"
|
||||
cp "$body" "$LOG_DIR/enterprise_copilot_thread_create.json"
|
||||
json_assert "$body" 'data.id && data.assistant && Number(data.assistant.id) === Number("'$captain_assistant_id'")' "Copilot thread create returns Chatwoot thread payload"
|
||||
created_id="$(json_value "$body" 'data.id')"
|
||||
|
||||
body="$(tmp_file)"
|
||||
authed_curl -o "$body" "http://$API_HOST:$API_PORT/api/v1/accounts/$account_id/captain/copilot_threads/$created_id/copilot_messages"
|
||||
cp "$body" "$LOG_DIR/enterprise_copilot_messages.json"
|
||||
json_assert "$body" 'data.payload && data.payload.length >= 1 && data.payload[0].copilot_thread' "Copilot thread messages return nested payload"
|
||||
|
||||
BOOT_BACKEND_RESULT="Passed /health during enterprise smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed API smoke" \
|
||||
INBOX_RESULT="Passed API smoke" \
|
||||
CONVERSATION_RESULT="Passed API smoke" \
|
||||
CRM_RESULT="Passed contact/company API smoke" \
|
||||
WIDGET_RESULT="Passed API smoke" \
|
||||
CSAT_RESULT="Passed public/account/download enterprise smoke" \
|
||||
ENTERPRISE_RESULT="Passed enterprise API smoke for SLA, CSAT, automation, macros, audit, custom roles, capacity, Captain, and Copilot" \
|
||||
write_report "Enterprise API smoke passed for reused Chatwoot enterprise paths; live enterprise browser navigation remains optional." "Command run: \`scripts/parity_frontend_smoke.sh --enterprise-smoke\`. Logs and payload captures are under \`$LOG_DIR\`. Next B12.3 browser work should load the enterprise screens through the reused Vite app."
|
||||
echo "enterprise smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
run_enterprise_browser_smoke() {
|
||||
run_enterprise_api_smoke
|
||||
wait_url "http://$FRONTEND_HOST:$FRONTEND_PORT" "Chatwoot Vite"
|
||||
GOCHAT_ROOT="$ROOT" \
|
||||
CHATWOOT_DIR="$CHATWOOT_DIR" \
|
||||
GOCHAT_SMOKE_LOG_DIR="$LOG_DIR" \
|
||||
GOCHAT_SMOKE_API_HOST="$API_HOST" \
|
||||
GOCHAT_SMOKE_API_PORT="$API_PORT" \
|
||||
GOCHAT_SMOKE_FRONTEND_HOST="$FRONTEND_HOST" \
|
||||
GOCHAT_SMOKE_FRONTEND_PORT="$FRONTEND_PORT" \
|
||||
node "$ROOT/scripts/parity_frontend_browser_smoke.mjs" --enterprise
|
||||
BOOT_BACKEND_RESULT="Passed /health during enterprise browser smoke" \
|
||||
BOOT_FRONTEND_RESULT="Passed Vite readiness during enterprise browser smoke" \
|
||||
AUTH_PROFILE_RESULT="Passed browser login plus API smoke" \
|
||||
INBOX_RESULT="Passed API smoke" \
|
||||
CONVERSATION_RESULT="Passed dashboard browser request plus API smoke" \
|
||||
CRM_RESULT="Passed contact/company API smoke" \
|
||||
WIDGET_RESULT="Passed API smoke" \
|
||||
CSAT_RESULT="Passed CSAT API/download and browser route requests" \
|
||||
ENTERPRISE_RESULT="Passed enterprise browser route requests for SLA, CSAT, automation, macros, audit, custom roles, capacity, Captain, and Copilot" \
|
||||
write_report "Enterprise browser smoke passed for reused Chatwoot enterprise route requests; enterprise API smoke passed." "Command run: \`scripts/parity_frontend_smoke.sh --enterprise-browser-smoke\`. Browser report: \`$LOG_DIR/browser-smoke-report.json\`."
|
||||
echo "enterprise browser smoke passed; report written to $REPORT_PATH"
|
||||
}
|
||||
|
||||
if [[ "$MODE" == "print" ]]; then
|
||||
print_commands
|
||||
exit 0
|
||||
@@ -345,6 +568,21 @@ if [[ "$MODE" == "api-smoke" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "browser-smoke" ]]; then
|
||||
run_browser_smoke
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "enterprise-smoke" ]]; then
|
||||
run_enterprise_api_smoke
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "enterprise-browser-smoke" ]]; then
|
||||
run_enterprise_browser_smoke
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
trap 'jobs -pr | xargs -r kill 2>/dev/null || true' EXIT
|
||||
|
||||
|
||||
Reference in New Issue
Block a user