Add customer mass import service with API route, test coverage, and e-conomic integration
This commit is contained in:
@@ -12,6 +12,21 @@ import { DEFAULT_CONFIG_FILE_NAME, DEFAULT_HOST_API_URL } from "./test-gateway.m
|
||||
const execFile = promisify(execFileCallback);
|
||||
const COMPOSE_SERVICES = ["traefik", "redis", "mysql-debug", "edge-broker", "php1", "caddy"];
|
||||
|
||||
function composeArgs(projectName, args) {
|
||||
return ["compose", "-p", projectName, ...args];
|
||||
}
|
||||
|
||||
async function resolveRootDir(scriptPath) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
try {
|
||||
await fs.access(path.join(cwd, "docker-compose.yml"));
|
||||
return cwd;
|
||||
} catch {
|
||||
return path.resolve(path.dirname(scriptPath), "..");
|
||||
}
|
||||
}
|
||||
|
||||
async function runCommand(command, args, { cwd, allowFailure = false, stdio = "pipe" } = {}) {
|
||||
if (stdio === "inherit") {
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -74,8 +89,8 @@ async function waitForCondition(predicate, { timeoutMs = 30_000, intervalMs = 50
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
async function ensureComposeServices(rootDir) {
|
||||
await runCommand("docker", ["compose", "up", "-d", ...COMPOSE_SERVICES], {
|
||||
async function ensureComposeServices(rootDir, composeProject) {
|
||||
await runCommand("docker", composeArgs(composeProject, ["up", "-d", ...COMPOSE_SERVICES]), {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
@@ -120,21 +135,20 @@ function parseLastJsonLine(output) {
|
||||
throw new Error(`Unable to parse JSON from command output:\n${output}`);
|
||||
}
|
||||
|
||||
async function runPhpFixture(rootDir, action, payload = null) {
|
||||
async function runPhpFixture(rootDir, composeProject, action, payload = null) {
|
||||
const encodedPayload = payload === null
|
||||
? ""
|
||||
: ` ${Buffer.from(JSON.stringify(payload), "utf8").toString("base64url")}`;
|
||||
const command = `cd /var/www/html && CONFIG_DB_TARGET=debug php tests/Support/EdgeGatewayE2eFixture.php ${action}${encodedPayload}`;
|
||||
|
||||
const result = await runCommand("docker", [
|
||||
"compose",
|
||||
const result = await runCommand("docker", composeArgs(composeProject, [
|
||||
"exec",
|
||||
"-T",
|
||||
"php1",
|
||||
"sh",
|
||||
"-lc",
|
||||
command,
|
||||
], {
|
||||
]), {
|
||||
cwd: rootDir,
|
||||
});
|
||||
|
||||
@@ -233,6 +247,7 @@ async function waitForSocketOpen(socket) {
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error("Timed out waiting for websocket open.")), 10_000);
|
||||
const socketUrl = typeof socket.url === "string" && socket.url !== "" ? ` (${socket.url})` : "";
|
||||
|
||||
const onOpen = () => {
|
||||
clearTimeout(timeout);
|
||||
@@ -241,11 +256,25 @@ async function waitForSocketOpen(socket) {
|
||||
|
||||
const onError = (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
if (error instanceof Error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const readyState = typeof socket.readyState === "number" ? socket.readyState : "unknown";
|
||||
reject(new Error(`Websocket failed to open${socketUrl}; readyState=${readyState}.`));
|
||||
};
|
||||
|
||||
const onClose = (event) => {
|
||||
clearTimeout(timeout);
|
||||
const code = event && typeof event === "object" && "code" in event ? event.code : "unknown";
|
||||
const reason = event && typeof event === "object" && "reason" in event ? event.reason : "";
|
||||
reject(new Error(`Websocket closed before open${socketUrl}; code=${code} reason=${reason || "none"}.`));
|
||||
};
|
||||
|
||||
onSocket(socket, "open", onOpen);
|
||||
onSocket(socket, "error", onError);
|
||||
onSocket(socket, "close", onClose);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,12 +311,15 @@ function collectMessages(rows) {
|
||||
|
||||
async function main() {
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const rootDir = path.resolve(path.dirname(scriptPath), "..");
|
||||
const rootDir = await resolveRootDir(scriptPath);
|
||||
const runId = randomUUID().slice(0, 8);
|
||||
const containerName = `truckwash-edge-e2e-${runId}`;
|
||||
const configDir = path.join(rootDir, ".tmp", "edge-gateway-e2e", runId);
|
||||
const configFilePath = path.join(configDir, DEFAULT_CONFIG_FILE_NAME);
|
||||
const baseUrl = process.env.EDGE_GATEWAY_E2E_BASE_URL || DEFAULT_HOST_API_URL;
|
||||
const composeProject =
|
||||
process.env.EDGE_GATEWAY_E2E_COMPOSE_PROJECT
|
||||
|| path.basename(rootDir);
|
||||
|
||||
let fixture = null;
|
||||
let gatewayId = null;
|
||||
@@ -295,10 +327,10 @@ async function main() {
|
||||
let shellSocket = null;
|
||||
|
||||
try {
|
||||
await ensureComposeServices(rootDir);
|
||||
await ensureComposeServices(rootDir, composeProject);
|
||||
await waitForApiReady(baseUrl);
|
||||
|
||||
fixture = await runPhpFixture(rootDir, "create");
|
||||
fixture = await runPhpFixture(rootDir, composeProject, "create");
|
||||
const authToken = String(fixture.auth_token || "");
|
||||
const departmentId = Number(fixture.department_id || 0);
|
||||
|
||||
@@ -383,6 +415,23 @@ async function main() {
|
||||
}
|
||||
);
|
||||
|
||||
await waitForCondition(
|
||||
async () => {
|
||||
const detail = await apiRequest(baseUrl, "GET", `/edge-gateways/${gatewayId}`, {
|
||||
token: authToken,
|
||||
});
|
||||
|
||||
return Boolean(
|
||||
detail?.data?.channel_status?.broker?.connected
|
||||
|| detail?.data?.metadata?.broker_connected
|
||||
);
|
||||
},
|
||||
{
|
||||
timeoutMs: 90_000,
|
||||
message: "Gateway never established a live broker connection after install.",
|
||||
}
|
||||
);
|
||||
|
||||
const WebSocketImpl = await loadWebSocketImplementation();
|
||||
const streamSession = await apiRequest(baseUrl, "POST", `/edge-gateways/${gatewayId}/stream-session`, {
|
||||
token: authToken,
|
||||
@@ -401,6 +450,13 @@ async function main() {
|
||||
{ timeoutMs: 15_000, message: "Gateway stream never became ready." }
|
||||
);
|
||||
|
||||
const readyMessage = streamMessages.find((message) => message?.type === "gateway.stream.ready");
|
||||
assert.equal(
|
||||
Boolean(readyMessage?.connected),
|
||||
true,
|
||||
"Gateway stream became ready before the broker reported the gateway as connected."
|
||||
);
|
||||
|
||||
const operationResponse = await apiRequest(baseUrl, "POST", `/edge-gateways/${gatewayId}/operations`, {
|
||||
token: authToken,
|
||||
body: {
|
||||
@@ -531,12 +587,6 @@ async function main() {
|
||||
closeSocket(shellSocket);
|
||||
closeSocket(streamSocket);
|
||||
|
||||
if (gatewayId !== null && fixture?.auth_token) {
|
||||
await apiRequest(baseUrl, "DELETE", `/edge-gateways/${gatewayId}`, {
|
||||
token: String(fixture.auth_token),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
await runCommand(process.execPath, [
|
||||
"scripts/test-gateway.mjs",
|
||||
"stop",
|
||||
@@ -547,8 +597,14 @@ async function main() {
|
||||
allowFailure: true,
|
||||
}).catch(() => {});
|
||||
|
||||
if (gatewayId !== null && fixture?.auth_token) {
|
||||
await apiRequest(baseUrl, "DELETE", `/edge-gateways/${gatewayId}`, {
|
||||
token: String(fixture.auth_token),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
if (fixture !== null) {
|
||||
await runPhpFixture(rootDir, "cleanup", fixture).catch(() => {});
|
||||
await runPhpFixture(rootDir, composeProject, "cleanup", fixture).catch(() => {});
|
||||
}
|
||||
|
||||
await fs.rm(configDir, { recursive: true, force: true }).catch(() => {});
|
||||
|
||||
@@ -21,6 +21,21 @@ export const DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 15;
|
||||
export const DEFAULT_INSTALLED_VERSION = "php-agent-v1";
|
||||
const DEFAULT_COMPOSE_SERVICES = ["traefik", "redis", "mysql-debug", "edge-broker", "caddy"];
|
||||
|
||||
function composeArgs(projectName, args) {
|
||||
return ["compose", "-p", projectName, ...args];
|
||||
}
|
||||
|
||||
async function resolveRootDir(scriptPath) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
try {
|
||||
await fs.access(path.join(cwd, "docker-compose.yml"));
|
||||
return cwd;
|
||||
} catch {
|
||||
return path.resolve(path.dirname(scriptPath), "..");
|
||||
}
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
process.stdout.write(`Usage:
|
||||
node scripts/test-gateway.mjs start [--install-token <token>] [--container-name <name>] [--hostname <hostname>]
|
||||
@@ -245,7 +260,7 @@ async function ensureComposeServices(rootDir, skipComposeUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
await runCommand("docker", ["compose", "up", "-d", ...DEFAULT_COMPOSE_SERVICES], {
|
||||
await runCommand("docker", composeArgs(resolveComposeProjectName(rootDir), ["up", "-d", ...DEFAULT_COMPOSE_SERVICES]), {
|
||||
cwd: rootDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
@@ -417,7 +432,7 @@ async function main() {
|
||||
}
|
||||
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const rootDir = path.resolve(path.dirname(scriptPath), "..");
|
||||
const rootDir = await resolveRootDir(scriptPath);
|
||||
const configDir = resolveConfigDirectory(rootDir, options.configDir);
|
||||
const configFilePath = path.join(configDir, DEFAULT_CONFIG_FILE_NAME);
|
||||
|
||||
@@ -485,8 +500,12 @@ Gateway ID: ${config.gatewayId ?? "unclaimed"}
|
||||
}
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
const currentRealPath = await fs.realpath(currentFilePath).catch(() => currentFilePath);
|
||||
const invokedScript = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
||||
if (invokedScript === path.resolve(currentFilePath)) {
|
||||
const invokedRealPath = invokedScript !== ""
|
||||
? await fs.realpath(invokedScript).catch(() => invokedScript)
|
||||
: "";
|
||||
if (invokedRealPath === currentRealPath) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
|
||||
Reference in New Issue
Block a user