Files
nocobase-mcp/test/crm-service.test.ts
T

194 lines
6.8 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { DEFAULT_CONFIG } from "../src/config.js";
import { CrmService } from "../src/crm-service.js";
import type { NocoBaseClient } from "../src/nocobase-client.js";
test("renders compact customer rows, labels, relations, timezone, and pagination", async () => {
const requests: Array<Record<string, unknown>> = [];
const fake = {
listRecords: async (resource: string, options: Record<string, unknown>) => {
requests.push({ resource, ...options });
return {
data: [
{
id: 42,
f_q2q60q7bqbg: "Acme",
kh_progress: "kh_p_inprogress",
f_225h6n6v654: "Referral",
kehu_followup_person: [{ id: 7, nickname: "Alice" }],
updatedAt: "2026-07-08T09:00:00Z",
},
],
meta: { count: 1 },
};
},
} as unknown as NocoBaseClient;
const config = structuredClone(DEFAULT_CONFIG);
config.customer.resource = "crm_customers";
config.customer.fields = [
"id",
"f_q2q60q7bqbg",
"kh_progress",
"f_225h6n6v654",
"kehu_followup_person",
"updatedAt",
];
config.customer.statusField = "kh_progress";
config.customer.sourceField = "f_225h6n6v654";
config.customer.ownerField = "kehu_followup_person";
const result = await new CrmService(config, fake).listCustomers({ limit: 20 });
assert.deepEqual(result.results, [
{
id: 42,
name: "Acme",
status: "进行中",
source: "Referral",
owners: [{ id: 7, name: "Alice" }],
updatedAt: "2026-07-08T17:00:00+08:00",
},
]);
assert.equal(result.total, 1);
assert.equal(result.timezone, "Asia/Shanghai");
assert.equal(requests[0]?.resource, "crm_customers");
});
test("implements exact arbitrary offsets across two NocoBase pages", async () => {
const pages: number[] = [];
const fake = {
listRecords: async (_resource: string, options: Record<string, unknown>) => {
const page = options.page as number;
pages.push(page);
const start = (page - 1) * 20;
return {
data: Array.from({ length: 20 }, (_, index) => ({
id: start + index,
name: `Customer ${start + index}`,
})),
meta: { count: 100 },
};
},
} as unknown as NocoBaseClient;
const result = await new CrmService(structuredClone(DEFAULT_CONFIG), fake).listCustomers({
limit: 20,
offset: 25,
});
assert.deepEqual(pages, [2, 3]);
assert.deepEqual(
(result.results as Array<Record<string, unknown>>).map((row) => row.id),
Array.from({ length: 20 }, (_, i) => 25 + i),
);
assert.equal(result.next_offset, 45);
});
test("resolves owner candidates and returns ambiguity as structured data", async () => {
const fake = {
listRecords: async (resource: string) => {
if (resource === "users") {
return {
data: [
{ id: 1, username: "alex", nickname: "Alex A" },
{ id: 2, username: "alex", nickname: "Alex B" },
],
};
}
return { data: [], meta: { count: 0 } };
},
} as unknown as NocoBaseClient;
const service = new CrmService(structuredClone(DEFAULT_CONFIG), fake);
const resolved = await service.resolveOwner("alex");
assert.equal(resolved.count, 2);
const listed = await service.listCustomers({ owner: "alex" });
assert.equal((listed.error as Record<string, unknown>).code, "owner_ambiguous");
});
test("health never returns the Authorize Key", async () => {
const fake = {
health: async () => ({ ok: true, endpoint: "https://nb.example", status: "authenticated" }),
} as unknown as NocoBaseClient;
const result = await new CrmService(structuredClone(DEFAULT_CONFIG), fake).crmHealth();
assert.equal(result.ok, true);
assert.doesNotMatch(JSON.stringify(result), /token|secret|authorizeKey/i);
});
test("all CRM service surfaces execute against the direct HTTP adapter contract", async () => {
const config = structuredClone(DEFAULT_CONFIG);
const rows: Record<string, Record<string, unknown>> = {
[config.customer.resource]: {
id: 1,
name: "Acme",
owner: [{ id: 10, nickname: "Alice" }],
updatedAt: "2026-01-01T00:00:00Z",
},
[config.contact.resource]: { id: 2, name: "Bob", customer: [{ id: 1, name: "Acme" }] },
[config.followup.resource]: {
id: 3,
followedAt: "2026-01-02T00:00:00Z",
customer: [{ id: 1, name: "Acme" }],
owner: [{ id: 10, nickname: "Alice" }],
content: "Followed up",
},
[config.user.resource]: {
id: 10,
username: "alice",
nickname: "Alice",
[config.team.userStatusField]: config.team.formalUserStatusValue,
},
[config.team.resource]: {
id: 20,
[config.team.nameField]: "Alpha",
[config.team.managerField]: [{ id: 10 }],
[config.team.memberField]: [{ id: 10 }],
},
[config.project.resource]: {
id: 30,
[config.project.nameField]: "Project",
[config.project.progressField]: config.project.progressValues.已报价,
[config.project.customerRelationField]: [{ id: 1, name: "Acme" }],
},
[config.project.updateResource]: {
id: 31,
[config.project.updateDateField]: "2026-01-03T00:00:00Z",
[config.project.updateContentField]: "Updated",
createdBy: [{ id: 10, nickname: "Alice" }],
},
};
const fake = {
health: async () => ({ ok: true, endpoint: "https://nb.example", status: "authenticated" }),
listCollectionFields: async () => ({ data: [] }),
getRecord: async (resource: string) => ({ data: rows[resource] }),
listRecords: async (resource: string) => ({
data: rows[resource] ? [rows[resource]] : [],
meta: { count: rows[resource] ? 1 : 0 },
}),
queryRecords: async (_resource: string, options: Record<string, unknown>) => {
const dimensions = options.dimensions as Array<Record<string, unknown>> | undefined;
const alias = dimensions?.[0]?.alias;
return { data: [{ ...(typeof alias === "string" ? { [alias]: "value" } : {}), count: 1 }] };
},
} as unknown as NocoBaseClient;
const service = new CrmService(config, fake);
const results = await Promise.all([
service.crmHealth(),
service.crmSchema(true),
service.crmConfigTemplate(),
service.listCustomers(),
service.getCustomer(1),
service.listContacts(),
service.listFollowups(),
service.listProjects(),
service.getProject(30),
service.resolveOwner("alice"),
service.listTeams({ includeMembers: true }),
service.resolveTeam("Alpha"),
service.getTeam(20),
service.crmOverview(),
service.crmFilterOptions("alice"),
service.teamCrmOverview("Alpha"),
service.getProjectTimeline(30),
service.listStaleCustomers({ asOf: "2026-08-15", inactiveDays: 30 }),
]);
assert.equal(results.length, 18);
assert.ok(results.every((result) => !("error" in result)));
});