343e6c50e3
✨ New Features: - Hide Health Check Logs toggle (PR #111 by @nyatoru) - Kilocode custom models endpoint + 26 models (PR #115 by @benzntech) 🐛 Bug Fixes: - iFlow 406 error fixed with IFlowExecutor HMAC-SHA256 signature (#114) - Filter parent model duplicates from endpoint lists (PR #112 by @nyatoru) 🧪 Tests: - 11 new IFlowExecutor unit tests - All 379 tests passing
151 lines
6.1 KiB
JavaScript
151 lines
6.1 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// IFlowExecutor Unit Tests
|
|
// Tests for HMAC-SHA256 signature, headers, URL building
|
|
// Fixes: https://github.com/diegosouzapw/OmniRoute/issues/114
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
const { IFlowExecutor } = await import("../../open-sse/executors/iflow.ts");
|
|
|
|
// ─── Constructor ──────────────────────────────────────────────
|
|
|
|
test("IFlowExecutor: constructor sets provider to 'iflow'", () => {
|
|
const executor = new IFlowExecutor();
|
|
assert.equal(executor.getProvider(), "iflow");
|
|
});
|
|
|
|
// ─── createIFlowSignature ─────────────────────────────────────
|
|
|
|
test("IFlowExecutor: createIFlowSignature returns valid HMAC-SHA256 hex", () => {
|
|
const executor = new IFlowExecutor();
|
|
const userAgent = "iFlow-Cli";
|
|
const sessionID = "session-test-123";
|
|
const timestamp = 1700000000000;
|
|
const apiKey = "test-api-key-secret";
|
|
|
|
const signature = executor.createIFlowSignature(userAgent, sessionID, timestamp, apiKey);
|
|
|
|
// Verify it's a valid hex string (64 chars for SHA256)
|
|
assert.match(signature, /^[0-9a-f]{64}$/);
|
|
|
|
// Verify reproducibility — same inputs produce same signature
|
|
const signature2 = executor.createIFlowSignature(userAgent, sessionID, timestamp, apiKey);
|
|
assert.equal(signature, signature2);
|
|
|
|
// Verify against manual HMAC computation
|
|
const payload = `${userAgent}:${sessionID}:${timestamp}`;
|
|
const expected = crypto.createHmac("sha256", apiKey).update(payload).digest("hex");
|
|
assert.equal(signature, expected);
|
|
});
|
|
|
|
test("IFlowExecutor: createIFlowSignature returns empty string when apiKey is empty", () => {
|
|
const executor = new IFlowExecutor();
|
|
const result = executor.createIFlowSignature("agent", "session", 123, "");
|
|
assert.equal(result, "");
|
|
});
|
|
|
|
test("IFlowExecutor: createIFlowSignature returns empty string when apiKey is null", () => {
|
|
const executor = new IFlowExecutor();
|
|
const result = executor.createIFlowSignature("agent", "session", 123, null);
|
|
assert.equal(result, "");
|
|
});
|
|
|
|
// ─── buildHeaders ─────────────────────────────────────────────
|
|
|
|
test("IFlowExecutor: buildHeaders includes iflow-specific headers", () => {
|
|
const executor = new IFlowExecutor();
|
|
const credentials = { apiKey: "test-key-123" };
|
|
|
|
const headers = executor.buildHeaders(credentials, true);
|
|
|
|
// Must include required iflow headers
|
|
assert.ok(headers["session-id"], "Missing session-id header");
|
|
assert.ok(headers["x-iflow-timestamp"], "Missing x-iflow-timestamp header");
|
|
assert.ok(headers["x-iflow-signature"], "Missing x-iflow-signature header");
|
|
|
|
// session-id format
|
|
assert.ok(
|
|
headers["session-id"].startsWith("session-"),
|
|
"session-id should start with 'session-'"
|
|
);
|
|
|
|
// timestamp is a number string
|
|
assert.match(headers["x-iflow-timestamp"], /^\d+$/);
|
|
|
|
// signature is hex
|
|
assert.match(headers["x-iflow-signature"], /^[0-9a-f]{64}$/);
|
|
|
|
// Authorization
|
|
assert.equal(headers["Authorization"], "Bearer test-key-123");
|
|
|
|
// Content-Type
|
|
assert.equal(headers["Content-Type"], "application/json");
|
|
|
|
// Streaming Accept
|
|
assert.equal(headers["Accept"], "text/event-stream");
|
|
});
|
|
|
|
test("IFlowExecutor: buildHeaders omits Accept header when stream is false", () => {
|
|
const executor = new IFlowExecutor();
|
|
const credentials = { apiKey: "test-key" };
|
|
|
|
const headers = executor.buildHeaders(credentials, false);
|
|
|
|
assert.equal(headers["Accept"], undefined);
|
|
});
|
|
|
|
test("IFlowExecutor: buildHeaders uses accessToken when apiKey is missing", () => {
|
|
const executor = new IFlowExecutor();
|
|
const credentials = { accessToken: "oauth-token-123" };
|
|
|
|
const headers = executor.buildHeaders(credentials);
|
|
|
|
assert.equal(headers["Authorization"], "Bearer oauth-token-123");
|
|
// Signature should still be generated using the accessToken
|
|
assert.ok(headers["x-iflow-signature"].length > 0);
|
|
});
|
|
|
|
test("IFlowExecutor: buildHeaders generates unique session IDs per call", () => {
|
|
const executor = new IFlowExecutor();
|
|
const credentials = { apiKey: "key" };
|
|
|
|
const headers1 = executor.buildHeaders(credentials);
|
|
const headers2 = executor.buildHeaders(credentials);
|
|
|
|
assert.notEqual(headers1["session-id"], headers2["session-id"]);
|
|
});
|
|
|
|
// ─── buildUrl ─────────────────────────────────────────────────
|
|
|
|
test("IFlowExecutor: buildUrl returns config baseUrl", () => {
|
|
const executor = new IFlowExecutor();
|
|
const url = executor.buildUrl("qwen3-coder-plus", true);
|
|
|
|
assert.equal(url, "https://apis.iflow.cn/v1/chat/completions");
|
|
});
|
|
|
|
// ─── transformRequest ─────────────────────────────────────────
|
|
|
|
test("IFlowExecutor: transformRequest passes body through unchanged", () => {
|
|
const executor = new IFlowExecutor();
|
|
const body = {
|
|
model: "deepseek-r1",
|
|
messages: [{ role: "user", content: "Hello" }],
|
|
stream: true,
|
|
};
|
|
|
|
const result = executor.transformRequest("deepseek-r1", body, true, {});
|
|
assert.deepEqual(result, body);
|
|
});
|
|
|
|
// ─── Integration: executor registry ───────────────────────────
|
|
|
|
test("IFlowExecutor: getExecutor('iflow') returns IFlowExecutor instance", async () => {
|
|
const { getExecutor } = await import("../../open-sse/executors/index.ts");
|
|
const executor = getExecutor("iflow");
|
|
assert.ok(executor instanceof IFlowExecutor, "Should return IFlowExecutor instance");
|
|
});
|