194 lines
6.8 KiB
TypeScript
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)));
|
|
});
|