Skip to content

Commit 457a9c5

Browse files
feat(integration): add more public APIs for integration scope (#1099)
* refactor(worker): move integration, chat and ai-agent handler data access into business * feat(integration): add more public APIs for integration scope
1 parent b6ed384 commit 457a9c5

176 files changed

Lines changed: 6688 additions & 4201 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/builder/__tests__/__snapshots__/public-spec-operations.test.ts.snap

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,11 +452,36 @@ exports[`public API spec — operation naming guard > operation list (operationI
452452
"operationId": "inboxTeams.list",
453453
"path": "/v1/teams",
454454
},
455+
{
456+
"method": "PUT",
457+
"operationId": "integrations.connectAiProvider",
458+
"path": "/v1/integrations/ai/{provider}",
459+
},
460+
{
461+
"method": "DELETE",
462+
"operationId": "integrations.disconnectAiProvider",
463+
"path": "/v1/integrations/ai/{provider}",
464+
},
465+
{
466+
"method": "GET",
467+
"operationId": "integrations.get",
468+
"path": "/v1/integrations/{id}",
469+
},
470+
{
471+
"method": "GET",
472+
"operationId": "integrations.getAiProvider",
473+
"path": "/v1/integrations/ai/{provider}",
474+
},
455475
{
456476
"method": "GET",
457477
"operationId": "integrations.list",
458478
"path": "/v1/integrations",
459479
},
480+
{
481+
"method": "GET",
482+
"operationId": "integrations.tokenErrors",
483+
"path": "/v1/integrations/status/token-errors",
484+
},
460485
{
461486
"method": "GET",
462487
"operationId": "keywords.list",

apps/builder/__tests__/create-api.action.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({
77
connect: vi.fn(),
88
findWorkspaceOrFail: vi.fn(),
99
createWorkspace: vi.fn(),
10+
hasWorkspaceAccess: vi.fn(async () => true),
1011
generateApiChannelToken: vi.fn(async () => ({
1112
token: "plain-token",
1213
tokenHash: "token-hash",
@@ -24,6 +25,7 @@ vi.mock("@/lib/safe-action", () => {
2425

2526
vi.mock("@chatbotx.io/business", () => ({
2627
assertPublicUrl: mocks.assertPublicUrl,
28+
hasWorkspaceAccess: mocks.hasWorkspaceAccess,
2729
integrationApiService: { connect: mocks.connect },
2830
workspaceService: {
2931
findOrFail: mocks.findWorkspaceOrFail,
@@ -52,6 +54,7 @@ type ActionHandler = (args: {
5254
describe("createApiAction", () => {
5355
beforeEach(() => {
5456
vi.clearAllMocks()
57+
mocks.hasWorkspaceAccess.mockResolvedValue(true)
5558
mocks.findWorkspaceOrFail.mockResolvedValue({
5659
id: "workspace-1",
5760
ownerId: "owner-1",
@@ -92,4 +95,20 @@ describe("createApiAction", () => {
9295
token: "plain-token",
9396
})
9497
})
98+
99+
test("rejects a workspaceId the caller is not a member of", async () => {
100+
mocks.hasWorkspaceAccess.mockResolvedValue(false)
101+
102+
await expect(
103+
(createApiAction as unknown as ActionHandler)({
104+
parsedInput: {
105+
workspaceId: "workspace-1",
106+
name: "Support API",
107+
},
108+
ctx: { user: { id: "intruder-1" } },
109+
}),
110+
).rejects.toThrow()
111+
112+
expect(mocks.connect).not.toHaveBeenCalled()
113+
})
95114
})

apps/builder/__tests__/disconnect-meta-actions.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ const mocks = vi.hoisted(() => {
2929
messengerDisconnect: vi.fn().mockResolvedValue(undefined),
3030
messengerDisconnectSafe: vi.fn(() => false),
3131
messengerExists: vi.fn().mockResolvedValue(false),
32+
messengerServiceDisconnect: vi.fn(
33+
(props: {
34+
id: string
35+
tx: { delete: (...args: unknown[]) => unknown }
36+
}) => Promise.resolve(props.tx.delete()),
37+
),
38+
instagramServiceDisconnect: vi.fn(
39+
(props: {
40+
id: string
41+
tx: { delete: (...args: unknown[]) => unknown }
42+
}) => Promise.resolve(props.tx.delete()),
43+
),
3244
instagramDisconnect: vi.fn().mockResolvedValue(undefined),
3345
instagramFacebookDisconnect: vi.fn().mockResolvedValue(undefined),
3446
subscribePageToAppWebhook: vi.fn().mockResolvedValue(undefined),
@@ -47,8 +59,16 @@ vi.mock("@chatbotx.io/business", () => ({
4759
tearDownForIntegration: mocks.coexistTearDownForIntegration,
4860
},
4961
inboxService: { disconnect: mocks.inboxDisconnect },
50-
instagramIntegrationService: { existsForPage: mocks.instagramExists },
51-
messengerIntegrationService: { existsForPage: mocks.messengerExists },
62+
instagramIntegrationService: {
63+
existsForPage: mocks.instagramExists,
64+
findByIdForWorkspace: mocks.findOrFail,
65+
disconnect: mocks.instagramServiceDisconnect,
66+
},
67+
messengerIntegrationService: {
68+
existsForPage: mocks.messengerExists,
69+
findByIdForWorkspace: mocks.findOrFail,
70+
disconnect: mocks.messengerServiceDisconnect,
71+
},
5272
workspaceService: { findById: mocks.workspaceFindById },
5373
}))
5474

apps/builder/__tests__/disconnect-whatsapp-action.test.ts

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import { beforeEach, describe, expect, test, vi } from "vitest"
44

5+
const LIVE_RUN_STATUSES = ["init", "running", "waiting"]
6+
57
const mocks = vi.hoisted(() => {
68
const txChain = {
79
set: vi.fn(),
@@ -30,34 +32,66 @@ const mocks = vi.hoisted(() => {
3032
}
3133
})
3234

35+
// Mirrors `integrationWhatsappService.disconnect`'s real transaction body —
36+
// the test asserts on these same tx calls, so the mock replicates them
37+
// rather than mocking `@chatbotx.io/business` transitively (which would
38+
// require booting the real business/database module graph).
39+
const integrationWhatsappServiceDisconnect = vi.fn(
40+
async (props: {
41+
integrationWhatsapp: { id: string; inboxId: string; phoneNumberId: string }
42+
ownerId: string
43+
workspaceId: string
44+
tx: typeof mocks.tx
45+
}) => {
46+
const tx = props.tx as unknown as {
47+
update: (arg?: unknown) => {
48+
set: (arg?: unknown) => { where: (arg?: unknown) => unknown }
49+
}
50+
delete: (arg?: unknown) => unknown
51+
}
52+
53+
tx.update()
54+
.set()
55+
.where({
56+
conditions: [
57+
{ field: "integrationId", value: props.integrationWhatsapp.id },
58+
{ field: "status", values: LIVE_RUN_STATUSES },
59+
],
60+
})
61+
tx.delete()
62+
await mocks.metaCapiDeleteByIntegration(
63+
{
64+
workspaceId: props.workspaceId,
65+
channel: "whatsapp",
66+
integrationId: props.integrationWhatsapp.id,
67+
},
68+
props.tx,
69+
)
70+
tx.delete({ id: "whatsappId" })
71+
await mocks.inboxDisconnect({
72+
inboxId: props.integrationWhatsapp.inboxId,
73+
ownerId: props.ownerId,
74+
workspaceId: props.workspaceId,
75+
reason: "manual",
76+
tx: props.tx,
77+
})
78+
},
79+
)
80+
3381
vi.mock("@chatbotx.io/business", () => ({
34-
inboxService: { disconnect: mocks.inboxDisconnect },
82+
integrationWhatsappService: {
83+
disconnect: integrationWhatsappServiceDisconnect,
84+
},
3585
workspaceService: { findById: mocks.workspaceFindById },
3686
}))
3787

3888
vi.mock("@chatbotx.io/database/client", () => ({
39-
and: vi.fn((...conditions: unknown[]) => ({ conditions })),
4089
db: { transaction: mocks.dbTransaction },
41-
eq: vi.fn((field: unknown, value: unknown) => ({ field, value })),
4290
findOrFail: mocks.findOrFail,
43-
inArray: vi.fn((field: unknown, values: unknown[]) => ({ field, values })),
44-
}))
45-
46-
vi.mock("@chatbotx.io/database/repositories", () => ({
47-
LIVE_RUN_STATUSES: ["init", "running", "waiting"],
48-
metaCapiEventRepository: {
49-
deleteByIntegration: mocks.metaCapiDeleteByIntegration,
50-
},
5191
}))
5292

5393
vi.mock("@chatbotx.io/database/schema", () => ({
54-
coexistSyncRunModel: {
55-
finishedAt: "finishedAt",
56-
integrationId: "integrationId",
57-
status: "status",
58-
},
5994
integrationWhatsappModel: { id: "whatsappId" },
60-
whatsappCoexistStagingModel: { phoneNumberId: "phoneNumberId" },
6195
}))
6296

6397
vi.mock("@chatbotx.io/integration-whatsapp", () => ({

apps/builder/__tests__/integration-tiktok-connect.action.test.ts

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,16 @@
33
import { beforeEach, describe, expect, test, vi } from "vitest"
44

55
const mocks = vi.hoisted(() => ({
6-
connectChannelIntegration: vi.fn(),
6+
connect: vi.fn(),
77
findWorkspaceById: vi.fn(),
88
transaction: vi.fn(),
9-
insert: vi.fn(),
10-
values: vi.fn(),
11-
onConflictDoUpdate: vi.fn(),
12-
returning: vi.fn(),
139
auditRecord: vi.fn(),
1410
handleRequest: vi.fn(),
15-
createId: vi.fn(() => "generated-integration-id"),
1611
redirect: vi.fn(),
1712
}))
1813

1914
vi.mock("@chatbotx.io/business", () => ({
20-
connectChannelIntegration: mocks.connectChannelIntegration,
15+
tiktokIntegrationService: { connect: mocks.connect },
2116
workspaceService: { findById: mocks.findWorkspaceById },
2217
}))
2318

@@ -40,14 +35,6 @@ vi.mock("@chatbotx.io/database/client", () => ({
4035
db: { transaction: mocks.transaction },
4136
}))
4237

43-
vi.mock("@chatbotx.io/database/schema", () => ({
44-
integrationTiktokModel: { id: "id", openId: "openId" },
45-
}))
46-
47-
vi.mock("@chatbotx.io/utils", () => ({
48-
createId: mocks.createId,
49-
}))
50-
5138
vi.mock("next/navigation", () => ({
5239
redirect: mocks.redirect,
5340
}))
@@ -74,25 +61,15 @@ describe("connectTiktokHandler", () => {
7461
},
7562
})
7663
mocks.transaction.mockImplementation(async (fn: (tx: unknown) => unknown) =>
77-
fn({
78-
insert: mocks.insert,
79-
}),
64+
fn({}),
8065
)
81-
mocks.insert.mockReturnValue({ values: mocks.values })
82-
mocks.values.mockReturnValue({
83-
onConflictDoUpdate: mocks.onConflictDoUpdate,
84-
})
85-
mocks.onConflictDoUpdate.mockReturnValue({ returning: mocks.returning })
86-
mocks.returning.mockResolvedValue([{ id: "existing-integration-id" }])
8766
})
8867

8968
test("records reconnect audit with the persisted TikTok integration id on conflict", async () => {
90-
mocks.connectChannelIntegration.mockImplementation(
91-
async ({ insertIntegration }) => {
92-
const integration = await insertIntegration("inbox-1", false)
93-
return { wasCreated: false, integration }
94-
},
95-
)
69+
mocks.connect.mockResolvedValue({
70+
wasCreated: false,
71+
integration: { id: "existing-integration-id" },
72+
})
9673

9774
await connectTiktokHandler({
9875
tiktokSettings: { clientId: "client", clientSecret: "secret" },
@@ -102,15 +79,14 @@ describe("connectTiktokHandler", () => {
10279
redirectUrl: "https://app.example.com/integrations/tiktok/callback",
10380
})
10481

105-
expect(mocks.values).toHaveBeenCalledWith(
82+
expect(mocks.connect).toHaveBeenCalledWith(
10683
expect.objectContaining({
107-
id: "generated-integration-id",
108-
inboxId: "inbox-1",
10984
workspaceId: "workspace-1",
11085
openId: "open-id-1",
86+
username: "shop_1",
87+
displayName: "TikTok Shop",
11188
}),
11289
)
113-
expect(mocks.returning).toHaveBeenCalledWith({ id: "id" })
11490
expect(mocks.auditRecord).toHaveBeenCalledTimes(1)
11591
expect(mocks.auditRecord).toHaveBeenCalledWith({
11692
userId: "admin-1",

0 commit comments

Comments
 (0)