-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathactions-surface.test.ts
More file actions
86 lines (76 loc) · 3.73 KB
/
Copy pathactions-surface.test.ts
File metadata and controls
86 lines (76 loc) · 3.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
/**
* `client.actions` — the SDK path to server-registered action handlers
* (`POST /api/v1/actions/...`), closing the largest #3563 gap: before this
* surface, the whole `/actions` domain was unreachable from the SDK.
*/
import { describe, it, expect, vi } from 'vitest';
import { ObjectStackClient } from './index';
function createMockClient(body: any, status = 200) {
const fetchMock = vi.fn().mockResolvedValue({
ok: status >= 200 && status < 300,
status,
statusText: status === 200 ? 'OK' : 'Error',
json: async () => body,
headers: new Headers()
});
const client = new ObjectStackClient({
baseUrl: 'http://localhost:3000',
fetch: fetchMock
});
return { client, fetchMock };
}
describe('client.actions', () => {
it('invoke POSTs to /api/v1/actions/:object/:action with recordId + params in the body', async () => {
const { client, fetchMock } = createMockClient({
success: true,
data: { success: true, data: { converted: true } },
});
const result = await client.actions.invoke('crm_lead', 'convert', {
recordId: 'lead_1',
params: { create_opportunity: true },
});
const [url, init] = fetchMock.mock.calls[0];
expect(String(url)).toBe('http://localhost:3000/api/v1/actions/crm_lead/convert');
expect(init.method).toBe('POST');
expect(JSON.parse(init.body)).toEqual({
recordId: 'lead_1',
params: { create_opportunity: true },
});
// unwrapResponse strips the transport envelope; the INNER envelope is
// the action handler's own result.
expect(result).toEqual({ success: true, data: { converted: true } });
});
it('invoke URL-encodes object and action names', async () => {
const { client, fetchMock } = createMockClient({ success: true, data: { success: true } });
await client.actions.invoke('my object', 'do/thing');
expect(String(fetchMock.mock.calls[0][0])).toBe(
'http://localhost:3000/api/v1/actions/my%20object/do%2Fthing',
);
});
it('invoke defaults params to {} and omits recordId when not given', async () => {
const { client, fetchMock } = createMockClient({ success: true, data: { success: true } });
await client.actions.invoke('crm_lead', 'recalculate');
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ params: {} });
});
it('invokeGlobal targets the wildcard shape /actions/global/:action', async () => {
const { client, fetchMock } = createMockClient({ success: true, data: { success: true } });
await client.actions.invokeGlobal('nightly_cleanup', { params: { dryRun: true } });
expect(String(fetchMock.mock.calls[0][0])).toBe(
'http://localhost:3000/api/v1/actions/global/nightly_cleanup',
);
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ params: { dryRun: true } });
});
it('surfaces the handler business failure without throwing (inner success:false)', async () => {
// The dispatcher reports handler throws as HTTP 200 with an inner
// `{ success:false, error }` — a toastable business failure, not a
// transport error (http-dispatcher.handleActions catch branch).
const { client } = createMockClient({
success: true,
data: { success: false, error: 'Lead is already converted' },
});
const result = await client.actions.invoke('crm_lead', 'convert', { recordId: 'lead_1' });
expect(result.success).toBe(false);
expect(result.error).toBe('Lead is already converted');
});
});