@oxyhq/core 3.18.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/OxyServices.js +3 -2
- package/dist/cjs/mixins/OxyServices.accounts.js +480 -0
- package/dist/cjs/mixins/OxyServices.connectedApps.js +73 -0
- package/dist/cjs/mixins/OxyServices.utility.js +3 -2
- package/dist/cjs/mixins/index.js +9 -6
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/OxyServices.js +3 -2
- package/dist/esm/mixins/OxyServices.accounts.js +477 -0
- package/dist/esm/mixins/OxyServices.connectedApps.js +70 -0
- package/dist/esm/mixins/OxyServices.utility.js +3 -2
- package/dist/esm/mixins/index.js +9 -6
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/OxyServices.d.ts +3 -2
- package/dist/types/index.d.ts +2 -3
- package/dist/types/mixins/OxyServices.accounts.d.ts +642 -0
- package/dist/types/mixins/OxyServices.auth.d.ts +1 -1
- package/dist/types/mixins/OxyServices.connectedApps.d.ts +168 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +6 -3
- package/dist/types/mixins/index.d.ts +3 -4
- package/package.json +1 -1
- package/src/OxyServices.ts +3 -2
- package/src/index.ts +33 -34
- package/src/mixins/OxyServices.accounts.ts +1079 -0
- package/src/mixins/OxyServices.auth.ts +1 -1
- package/src/mixins/OxyServices.connectedApps.ts +165 -0
- package/src/mixins/OxyServices.utility.ts +7 -4
- package/src/mixins/__tests__/accounts.test.ts +667 -0
- package/src/mixins/__tests__/connectedApps.test.ts +1 -1
- package/src/mixins/index.ts +11 -9
- package/dist/cjs/mixins/OxyServices.applications.js +0 -350
- package/dist/cjs/mixins/OxyServices.managedAccounts.js +0 -143
- package/dist/cjs/mixins/OxyServices.workspaces.js +0 -181
- package/dist/esm/mixins/OxyServices.applications.js +0 -347
- package/dist/esm/mixins/OxyServices.managedAccounts.js +0 -140
- package/dist/esm/mixins/OxyServices.workspaces.js +0 -178
- package/dist/types/mixins/OxyServices.applications.d.ts +0 -496
- package/dist/types/mixins/OxyServices.managedAccounts.d.ts +0 -145
- package/dist/types/mixins/OxyServices.workspaces.d.ts +0 -219
- package/src/mixins/OxyServices.applications.ts +0 -773
- package/src/mixins/OxyServices.managedAccounts.ts +0 -173
- package/src/mixins/OxyServices.workspaces.ts +0 -351
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accounts Mixin Tests (unified account graph)
|
|
3
|
+
*
|
|
4
|
+
* Exercises the typed helpers around `/accounts/...`. We stub `makeRequest` so
|
|
5
|
+
* the tests run without a network or a database — what we care about here is
|
|
6
|
+
* request shape (method, URL, query string, body, cache options), response
|
|
7
|
+
* envelope unwrapping (`{ accounts }` / `{ account }` / `{ members }` /
|
|
8
|
+
* `{ member }` / `{ credentials }` / `{ application }` / `{ applications }`),
|
|
9
|
+
* default fallbacks on missing fields, path-segment URL-encoding, and cache
|
|
10
|
+
* invalidation on writes (the `clearCacheEntry` / `clearCacheByPrefix`
|
|
11
|
+
* discipline), covering both the account graph and the applications owned
|
|
12
|
+
* within it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { OxyServices } from '../../OxyServices';
|
|
16
|
+
import type { User } from '../../models/interfaces';
|
|
17
|
+
import type {
|
|
18
|
+
AccountNode,
|
|
19
|
+
AccountMember,
|
|
20
|
+
AccountCredential,
|
|
21
|
+
AccountCredentialWithSecret,
|
|
22
|
+
RotateAccountCredentialResult,
|
|
23
|
+
Application,
|
|
24
|
+
ApplicationCredential,
|
|
25
|
+
ApplicationCredentialWithSecret,
|
|
26
|
+
RotateApplicationCredentialResult,
|
|
27
|
+
ApplicationUsageStats,
|
|
28
|
+
} from '../OxyServices.accounts';
|
|
29
|
+
|
|
30
|
+
const setAccessTokenForTest = (oxy: OxyServices): void => {
|
|
31
|
+
oxy.httpService.setTokens('test-token');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const userFixture: User = {
|
|
35
|
+
id: 'acc1',
|
|
36
|
+
publicKey: 'pk-acc1',
|
|
37
|
+
username: 'oxy-org',
|
|
38
|
+
name: { displayName: 'Oxy Org' },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const memberFixture: AccountMember = {
|
|
42
|
+
_id: 'm1',
|
|
43
|
+
accountId: 'acc1',
|
|
44
|
+
memberUserId: 'u2',
|
|
45
|
+
role: 'editor',
|
|
46
|
+
permissions: ['account:read', 'apps:read'],
|
|
47
|
+
inherit: true,
|
|
48
|
+
status: 'active',
|
|
49
|
+
createdAt: '2026-06-29T00:00:00.000Z',
|
|
50
|
+
updatedAt: '2026-06-29T00:00:00.000Z',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const accountNodeFixture: AccountNode = {
|
|
54
|
+
accountId: 'acc1',
|
|
55
|
+
kind: 'organization',
|
|
56
|
+
parentAccountId: 'root1',
|
|
57
|
+
account: userFixture,
|
|
58
|
+
relationship: 'owner',
|
|
59
|
+
callerMembership: { ...memberFixture, role: 'owner', source: 'direct' },
|
|
60
|
+
childCount: 2,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const credentialFixture: AccountCredential = {
|
|
64
|
+
_id: 'cred1',
|
|
65
|
+
accountId: 'acc1',
|
|
66
|
+
name: 'bot-prod',
|
|
67
|
+
publicKey: 'oxy_dk_abc',
|
|
68
|
+
type: 'service',
|
|
69
|
+
environment: 'production',
|
|
70
|
+
scopes: ['user:read'],
|
|
71
|
+
status: 'active',
|
|
72
|
+
createdByUserId: 'u1',
|
|
73
|
+
createdAt: '2026-06-29T00:00:00.000Z',
|
|
74
|
+
updatedAt: '2026-06-29T00:00:00.000Z',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const appFixture: Application = {
|
|
78
|
+
_id: 'app1',
|
|
79
|
+
name: 'Mention',
|
|
80
|
+
type: 'first_party',
|
|
81
|
+
status: 'active',
|
|
82
|
+
isOfficial: true,
|
|
83
|
+
isInternal: false,
|
|
84
|
+
capabilities: [],
|
|
85
|
+
redirectUris: ['https://mention.earth/__oxy/sso-callback'],
|
|
86
|
+
scopes: ['profile'],
|
|
87
|
+
createdByUserId: 'u1',
|
|
88
|
+
ownerAccountId: 'acc1',
|
|
89
|
+
createdAt: '2026-06-29T00:00:00.000Z',
|
|
90
|
+
updatedAt: '2026-06-29T00:00:00.000Z',
|
|
91
|
+
callerMembership: { ...memberFixture, role: 'admin', source: 'inherited' },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const appCredentialFixture: ApplicationCredential = {
|
|
95
|
+
_id: 'appcred1',
|
|
96
|
+
applicationId: 'app1',
|
|
97
|
+
name: 'prod-client',
|
|
98
|
+
publicKey: 'oxy_dk_app',
|
|
99
|
+
type: 'confidential',
|
|
100
|
+
environment: 'production',
|
|
101
|
+
scopes: ['profile'],
|
|
102
|
+
status: 'active',
|
|
103
|
+
createdByUserId: 'u1',
|
|
104
|
+
createdAt: '2026-06-29T00:00:00.000Z',
|
|
105
|
+
updatedAt: '2026-06-29T00:00:00.000Z',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
describe('OxyServices.accounts', () => {
|
|
109
|
+
let oxy: OxyServices;
|
|
110
|
+
let makeRequestSpy: jest.SpyInstance;
|
|
111
|
+
let clearEntrySpy: jest.SpyInstance;
|
|
112
|
+
let clearPrefixSpy: jest.SpyInstance;
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
oxy = new OxyServices({ baseURL: 'http://test.invalid' });
|
|
116
|
+
setAccessTokenForTest(oxy);
|
|
117
|
+
makeRequestSpy = jest.spyOn(oxy, 'makeRequest');
|
|
118
|
+
clearEntrySpy = jest.spyOn(oxy, 'clearCacheEntry');
|
|
119
|
+
clearPrefixSpy = jest.spyOn(oxy, 'clearCacheByPrefix');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
makeRequestSpy.mockRestore();
|
|
124
|
+
clearEntrySpy.mockRestore();
|
|
125
|
+
clearPrefixSpy.mockRestore();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('listAccounts', () => {
|
|
129
|
+
it('unwraps the `accounts` array and caches the flat read', async () => {
|
|
130
|
+
makeRequestSpy.mockResolvedValue({ accounts: [accountNodeFixture] });
|
|
131
|
+
|
|
132
|
+
const result = await oxy.listAccounts();
|
|
133
|
+
|
|
134
|
+
expect(result).toEqual([accountNodeFixture]);
|
|
135
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
136
|
+
'GET',
|
|
137
|
+
'/accounts',
|
|
138
|
+
undefined,
|
|
139
|
+
expect.objectContaining({ cache: true }),
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('requests the tree variant as a distinct cache-keyed path', async () => {
|
|
144
|
+
makeRequestSpy.mockResolvedValue({ accounts: [accountNodeFixture] });
|
|
145
|
+
await oxy.listAccounts({ tree: true });
|
|
146
|
+
expect(makeRequestSpy.mock.calls[0][1]).toBe('/accounts?tree=true');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns an empty array when `accounts` is absent', async () => {
|
|
150
|
+
makeRequestSpy.mockResolvedValue({});
|
|
151
|
+
expect(await oxy.listAccounts()).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('getAccount', () => {
|
|
156
|
+
it('unwraps the `account` object and caches the read', async () => {
|
|
157
|
+
makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
|
|
158
|
+
|
|
159
|
+
const result = await oxy.getAccount('acc1');
|
|
160
|
+
|
|
161
|
+
expect(result).toEqual(accountNodeFixture);
|
|
162
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
163
|
+
'GET',
|
|
164
|
+
'/accounts/acc1',
|
|
165
|
+
undefined,
|
|
166
|
+
expect.objectContaining({ cache: true }),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('URL-encodes the accountId path segment', async () => {
|
|
171
|
+
makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
|
|
172
|
+
await oxy.getAccount('a b/c');
|
|
173
|
+
expect(makeRequestSpy.mock.calls[0][1]).toBe('/accounts/a%20b%2Fc');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('createAccount', () => {
|
|
178
|
+
it('posts the payload, unwraps `account`, and busts every list', async () => {
|
|
179
|
+
makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
|
|
180
|
+
|
|
181
|
+
const result = await oxy.createAccount({ kind: 'organization', username: 'oxy-org' });
|
|
182
|
+
|
|
183
|
+
expect(result).toEqual(accountNodeFixture);
|
|
184
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
185
|
+
'POST',
|
|
186
|
+
'/accounts',
|
|
187
|
+
{ kind: 'organization', username: 'oxy-org' },
|
|
188
|
+
expect.objectContaining({ cache: false }),
|
|
189
|
+
);
|
|
190
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts');
|
|
191
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/accounts?');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('updateAccount', () => {
|
|
196
|
+
it('patches, unwraps `account`, and busts the detail + lists', async () => {
|
|
197
|
+
makeRequestSpy.mockResolvedValue({ account: accountNodeFixture });
|
|
198
|
+
|
|
199
|
+
const result = await oxy.updateAccount('acc1', { bio: 'hello' });
|
|
200
|
+
|
|
201
|
+
expect(result).toEqual(accountNodeFixture);
|
|
202
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
203
|
+
'PATCH',
|
|
204
|
+
'/accounts/acc1',
|
|
205
|
+
{ bio: 'hello' },
|
|
206
|
+
expect.objectContaining({ cache: false }),
|
|
207
|
+
);
|
|
208
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1');
|
|
209
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts');
|
|
210
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/accounts?');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('archiveAccount', () => {
|
|
215
|
+
it('deletes and busts the detail, members, credentials, and lists', async () => {
|
|
216
|
+
makeRequestSpy.mockResolvedValue({ success: true });
|
|
217
|
+
|
|
218
|
+
const result = await oxy.archiveAccount('acc1');
|
|
219
|
+
|
|
220
|
+
expect(result).toEqual({ success: true });
|
|
221
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
222
|
+
'DELETE',
|
|
223
|
+
'/accounts/acc1',
|
|
224
|
+
undefined,
|
|
225
|
+
expect.objectContaining({ cache: false }),
|
|
226
|
+
);
|
|
227
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1');
|
|
228
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/members');
|
|
229
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/credentials');
|
|
230
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts');
|
|
231
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/accounts?');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('listChildAccounts', () => {
|
|
236
|
+
it('unwraps the `accounts` array and caches the read', async () => {
|
|
237
|
+
makeRequestSpy.mockResolvedValue({ accounts: [accountNodeFixture] });
|
|
238
|
+
|
|
239
|
+
const result = await oxy.listChildAccounts('acc1');
|
|
240
|
+
|
|
241
|
+
expect(result).toEqual([accountNodeFixture]);
|
|
242
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
243
|
+
'GET',
|
|
244
|
+
'/accounts/acc1/children',
|
|
245
|
+
undefined,
|
|
246
|
+
expect.objectContaining({ cache: true }),
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns an empty array when `accounts` is absent', async () => {
|
|
251
|
+
makeRequestSpy.mockResolvedValue({});
|
|
252
|
+
expect(await oxy.listChildAccounts('acc1')).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('listAccountMembers', () => {
|
|
257
|
+
it('unwraps the `members` array and caches the read', async () => {
|
|
258
|
+
makeRequestSpy.mockResolvedValue({ members: [memberFixture] });
|
|
259
|
+
|
|
260
|
+
const result = await oxy.listAccountMembers('acc1');
|
|
261
|
+
|
|
262
|
+
expect(result).toEqual([memberFixture]);
|
|
263
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
264
|
+
'GET',
|
|
265
|
+
'/accounts/acc1/members',
|
|
266
|
+
undefined,
|
|
267
|
+
expect.objectContaining({ cache: true }),
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('returns an empty array when `members` is absent', async () => {
|
|
272
|
+
makeRequestSpy.mockResolvedValue({});
|
|
273
|
+
expect(await oxy.listAccountMembers('acc1')).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('inviteAccountMember', () => {
|
|
278
|
+
it('posts the invite, unwraps `member`, and busts the membership cache', async () => {
|
|
279
|
+
makeRequestSpy.mockResolvedValue({ member: memberFixture });
|
|
280
|
+
|
|
281
|
+
const result = await oxy.inviteAccountMember('acc1', {
|
|
282
|
+
usernameOrEmail: 'alice',
|
|
283
|
+
role: 'editor',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result).toEqual(memberFixture);
|
|
287
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
288
|
+
'POST',
|
|
289
|
+
'/accounts/acc1/members',
|
|
290
|
+
{ usernameOrEmail: 'alice', role: 'editor' },
|
|
291
|
+
expect.objectContaining({ cache: false }),
|
|
292
|
+
);
|
|
293
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/members');
|
|
294
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('updateAccountMember', () => {
|
|
299
|
+
it('patches the member, unwraps `member`, encodes ids, and busts membership', async () => {
|
|
300
|
+
makeRequestSpy.mockResolvedValue({ member: memberFixture });
|
|
301
|
+
|
|
302
|
+
const result = await oxy.updateAccountMember('acc1', 'm 1', { role: 'admin' });
|
|
303
|
+
|
|
304
|
+
expect(result).toEqual(memberFixture);
|
|
305
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
306
|
+
'PATCH',
|
|
307
|
+
'/accounts/acc1/members/m%201',
|
|
308
|
+
{ role: 'admin' },
|
|
309
|
+
expect.objectContaining({ cache: false }),
|
|
310
|
+
);
|
|
311
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/members');
|
|
312
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('removeAccountMember', () => {
|
|
317
|
+
it('deletes the member and busts the membership cache', async () => {
|
|
318
|
+
makeRequestSpy.mockResolvedValue({ success: true });
|
|
319
|
+
|
|
320
|
+
const result = await oxy.removeAccountMember('acc1', 'm1');
|
|
321
|
+
|
|
322
|
+
expect(result).toEqual({ success: true });
|
|
323
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
324
|
+
'DELETE',
|
|
325
|
+
'/accounts/acc1/members/m1',
|
|
326
|
+
undefined,
|
|
327
|
+
expect.objectContaining({ cache: false }),
|
|
328
|
+
);
|
|
329
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/members');
|
|
330
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('transferAccountOwnership', () => {
|
|
335
|
+
it('posts the transfer and busts both membership and the lists', async () => {
|
|
336
|
+
makeRequestSpy.mockResolvedValue({ success: true });
|
|
337
|
+
|
|
338
|
+
const result = await oxy.transferAccountOwnership('acc1', { userId: 'u2' });
|
|
339
|
+
|
|
340
|
+
expect(result).toEqual({ success: true });
|
|
341
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
342
|
+
'POST',
|
|
343
|
+
'/accounts/acc1/transfer-ownership',
|
|
344
|
+
{ userId: 'u2' },
|
|
345
|
+
expect.objectContaining({ cache: false }),
|
|
346
|
+
);
|
|
347
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/members');
|
|
348
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1');
|
|
349
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts');
|
|
350
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/accounts?');
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('listAccountApps', () => {
|
|
355
|
+
it('queries /applications by ownerAccountId and unwraps `applications`', async () => {
|
|
356
|
+
makeRequestSpy.mockResolvedValue({ applications: [appFixture] });
|
|
357
|
+
|
|
358
|
+
const result = await oxy.listAccountApps('acc1');
|
|
359
|
+
|
|
360
|
+
expect(result).toEqual([appFixture]);
|
|
361
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
362
|
+
'GET',
|
|
363
|
+
'/applications?ownerAccountId=acc1',
|
|
364
|
+
undefined,
|
|
365
|
+
expect.objectContaining({ cache: true }),
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('URL-encodes the ownerAccountId query value', async () => {
|
|
370
|
+
makeRequestSpy.mockResolvedValue({ applications: [] });
|
|
371
|
+
await oxy.listAccountApps('a b/c');
|
|
372
|
+
expect(makeRequestSpy.mock.calls[0][1]).toBe('/applications?ownerAccountId=a%20b%2Fc');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('returns an empty array when `applications` is absent', async () => {
|
|
376
|
+
makeRequestSpy.mockResolvedValue({});
|
|
377
|
+
expect(await oxy.listAccountApps('acc1')).toEqual([]);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('listAccountCredentials', () => {
|
|
382
|
+
it('unwraps the `credentials` array and caches the read', async () => {
|
|
383
|
+
makeRequestSpy.mockResolvedValue({ credentials: [credentialFixture] });
|
|
384
|
+
|
|
385
|
+
const result = await oxy.listAccountCredentials('acc1');
|
|
386
|
+
|
|
387
|
+
expect(result).toEqual([credentialFixture]);
|
|
388
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
389
|
+
'GET',
|
|
390
|
+
'/accounts/acc1/credentials',
|
|
391
|
+
undefined,
|
|
392
|
+
expect.objectContaining({ cache: true }),
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('returns an empty array when `credentials` is absent', async () => {
|
|
397
|
+
makeRequestSpy.mockResolvedValue({});
|
|
398
|
+
expect(await oxy.listAccountCredentials('acc1')).toEqual([]);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe('createAccountCredential', () => {
|
|
403
|
+
it('posts the config, returns the secret result, and busts the credentials cache', async () => {
|
|
404
|
+
const created: AccountCredentialWithSecret = {
|
|
405
|
+
credential: credentialFixture,
|
|
406
|
+
secret: 'sk_once',
|
|
407
|
+
};
|
|
408
|
+
makeRequestSpy.mockResolvedValue(created);
|
|
409
|
+
|
|
410
|
+
const result = await oxy.createAccountCredential('acc1', {
|
|
411
|
+
name: 'bot-prod',
|
|
412
|
+
environment: 'production',
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
expect(result).toEqual(created);
|
|
416
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
417
|
+
'POST',
|
|
418
|
+
'/accounts/acc1/credentials',
|
|
419
|
+
{ name: 'bot-prod', environment: 'production' },
|
|
420
|
+
expect.objectContaining({ cache: false }),
|
|
421
|
+
);
|
|
422
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/credentials');
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe('rotateAccountCredential', () => {
|
|
427
|
+
it('posts to /rotate, encodes ids, returns the audit result, and busts credentials', async () => {
|
|
428
|
+
const rotated: RotateAccountCredentialResult = {
|
|
429
|
+
credential: { ...credentialFixture, _id: 'cred2' },
|
|
430
|
+
secret: 'sk_new',
|
|
431
|
+
rotatedFrom: 'cred1',
|
|
432
|
+
graceExpiresAt: '2026-07-06T00:00:00.000Z',
|
|
433
|
+
};
|
|
434
|
+
makeRequestSpy.mockResolvedValue(rotated);
|
|
435
|
+
|
|
436
|
+
const result = await oxy.rotateAccountCredential('acc1', 'cred 1');
|
|
437
|
+
|
|
438
|
+
expect(result).toEqual(rotated);
|
|
439
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
440
|
+
'POST',
|
|
441
|
+
'/accounts/acc1/credentials/cred%201/rotate',
|
|
442
|
+
undefined,
|
|
443
|
+
expect.objectContaining({ cache: false }),
|
|
444
|
+
);
|
|
445
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/credentials');
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('revokeAccountCredential', () => {
|
|
450
|
+
it('deletes the credential and busts the credentials cache', async () => {
|
|
451
|
+
makeRequestSpy.mockResolvedValue({ success: true });
|
|
452
|
+
|
|
453
|
+
const result = await oxy.revokeAccountCredential('acc1', 'cred1');
|
|
454
|
+
|
|
455
|
+
expect(result).toEqual({ success: true });
|
|
456
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
457
|
+
'DELETE',
|
|
458
|
+
'/accounts/acc1/credentials/cred1',
|
|
459
|
+
undefined,
|
|
460
|
+
expect.objectContaining({ cache: false }),
|
|
461
|
+
);
|
|
462
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/accounts/acc1/credentials');
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('createApp', () => {
|
|
467
|
+
it('posts the payload, unwraps `application`, and busts every app list', async () => {
|
|
468
|
+
makeRequestSpy.mockResolvedValue({ application: appFixture });
|
|
469
|
+
|
|
470
|
+
const result = await oxy.createApp({ name: 'Mention', ownerAccountId: 'acc1' });
|
|
471
|
+
|
|
472
|
+
expect(result).toEqual(appFixture);
|
|
473
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
474
|
+
'POST',
|
|
475
|
+
'/applications',
|
|
476
|
+
{ name: 'Mention', ownerAccountId: 'acc1' },
|
|
477
|
+
expect.objectContaining({ cache: false }),
|
|
478
|
+
);
|
|
479
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications');
|
|
480
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/applications?');
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('getApp', () => {
|
|
485
|
+
it('unwraps `application`, encodes the id, and caches the read', async () => {
|
|
486
|
+
makeRequestSpy.mockResolvedValue({ application: appFixture });
|
|
487
|
+
|
|
488
|
+
const result = await oxy.getApp('app 1');
|
|
489
|
+
|
|
490
|
+
expect(result).toEqual(appFixture);
|
|
491
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
492
|
+
'GET',
|
|
493
|
+
'/applications/app%201',
|
|
494
|
+
undefined,
|
|
495
|
+
expect.objectContaining({ cache: true }),
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('updateApp', () => {
|
|
501
|
+
it('patches, unwraps `application`, and busts the detail + app lists', async () => {
|
|
502
|
+
makeRequestSpy.mockResolvedValue({ application: appFixture });
|
|
503
|
+
|
|
504
|
+
const result = await oxy.updateApp('app1', { description: 'Social' });
|
|
505
|
+
|
|
506
|
+
expect(result).toEqual(appFixture);
|
|
507
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
508
|
+
'PATCH',
|
|
509
|
+
'/applications/app1',
|
|
510
|
+
{ description: 'Social' },
|
|
511
|
+
expect.objectContaining({ cache: false }),
|
|
512
|
+
);
|
|
513
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications/app1');
|
|
514
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications');
|
|
515
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/applications?');
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('deleteApp', () => {
|
|
520
|
+
it('deletes and busts the detail, credentials, and app lists', async () => {
|
|
521
|
+
makeRequestSpy.mockResolvedValue({ success: true });
|
|
522
|
+
|
|
523
|
+
const result = await oxy.deleteApp('app1');
|
|
524
|
+
|
|
525
|
+
expect(result).toEqual({ success: true });
|
|
526
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
527
|
+
'DELETE',
|
|
528
|
+
'/applications/app1',
|
|
529
|
+
undefined,
|
|
530
|
+
expect.objectContaining({ cache: false }),
|
|
531
|
+
);
|
|
532
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications/app1');
|
|
533
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications/app1/credentials');
|
|
534
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications');
|
|
535
|
+
expect(clearPrefixSpy).toHaveBeenCalledWith('GET:/applications?');
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe('listAppCredentials', () => {
|
|
540
|
+
it('unwraps the `credentials` array and caches the read', async () => {
|
|
541
|
+
makeRequestSpy.mockResolvedValue({ credentials: [appCredentialFixture] });
|
|
542
|
+
|
|
543
|
+
const result = await oxy.listAppCredentials('app1');
|
|
544
|
+
|
|
545
|
+
expect(result).toEqual([appCredentialFixture]);
|
|
546
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
547
|
+
'GET',
|
|
548
|
+
'/applications/app1/credentials',
|
|
549
|
+
undefined,
|
|
550
|
+
expect.objectContaining({ cache: true }),
|
|
551
|
+
);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('returns an empty array when `credentials` is absent', async () => {
|
|
555
|
+
makeRequestSpy.mockResolvedValue({});
|
|
556
|
+
expect(await oxy.listAppCredentials('app1')).toEqual([]);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe('createAppCredential', () => {
|
|
561
|
+
it('posts the config, returns the secret result, and busts the credentials cache', async () => {
|
|
562
|
+
const created: ApplicationCredentialWithSecret = {
|
|
563
|
+
credential: appCredentialFixture,
|
|
564
|
+
secret: 'sk_app_once',
|
|
565
|
+
};
|
|
566
|
+
makeRequestSpy.mockResolvedValue(created);
|
|
567
|
+
|
|
568
|
+
const result = await oxy.createAppCredential('app1', {
|
|
569
|
+
name: 'prod-client',
|
|
570
|
+
type: 'confidential',
|
|
571
|
+
environment: 'production',
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(result).toEqual(created);
|
|
575
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
576
|
+
'POST',
|
|
577
|
+
'/applications/app1/credentials',
|
|
578
|
+
{ name: 'prod-client', type: 'confidential', environment: 'production' },
|
|
579
|
+
expect.objectContaining({ cache: false }),
|
|
580
|
+
);
|
|
581
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications/app1/credentials');
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
describe('rotateAppCredential', () => {
|
|
586
|
+
it('posts to /rotate, encodes ids, returns the audit result, and busts credentials', async () => {
|
|
587
|
+
const rotated: RotateApplicationCredentialResult = {
|
|
588
|
+
credential: { ...appCredentialFixture, _id: 'appcred2' },
|
|
589
|
+
secret: 'sk_app_new',
|
|
590
|
+
rotatedFrom: 'appcred1',
|
|
591
|
+
graceExpiresAt: '2026-07-06T00:00:00.000Z',
|
|
592
|
+
};
|
|
593
|
+
makeRequestSpy.mockResolvedValue(rotated);
|
|
594
|
+
|
|
595
|
+
const result = await oxy.rotateAppCredential('app1', 'appcred 1');
|
|
596
|
+
|
|
597
|
+
expect(result).toEqual(rotated);
|
|
598
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
599
|
+
'POST',
|
|
600
|
+
'/applications/app1/credentials/appcred%201/rotate',
|
|
601
|
+
undefined,
|
|
602
|
+
expect.objectContaining({ cache: false }),
|
|
603
|
+
);
|
|
604
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications/app1/credentials');
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe('revokeAppCredential', () => {
|
|
609
|
+
it('deletes the credential and busts the credentials cache', async () => {
|
|
610
|
+
makeRequestSpy.mockResolvedValue({ success: true });
|
|
611
|
+
|
|
612
|
+
const result = await oxy.revokeAppCredential('app1', 'appcred1');
|
|
613
|
+
|
|
614
|
+
expect(result).toEqual({ success: true });
|
|
615
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
616
|
+
'DELETE',
|
|
617
|
+
'/applications/app1/credentials/appcred1',
|
|
618
|
+
undefined,
|
|
619
|
+
expect.objectContaining({ cache: false }),
|
|
620
|
+
);
|
|
621
|
+
expect(clearEntrySpy).toHaveBeenCalledWith('GET:/applications/app1/credentials');
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
describe('getAppUsage', () => {
|
|
626
|
+
it('passes the period query and caches the read', async () => {
|
|
627
|
+
const usage: ApplicationUsageStats = {
|
|
628
|
+
summary: {
|
|
629
|
+
totalRequests: 10,
|
|
630
|
+
totalTokens: 0,
|
|
631
|
+
totalCredits: 0,
|
|
632
|
+
avgResponseTime: 5,
|
|
633
|
+
successfulRequests: 10,
|
|
634
|
+
errorRequests: 0,
|
|
635
|
+
},
|
|
636
|
+
byDay: [],
|
|
637
|
+
byEndpoint: [],
|
|
638
|
+
};
|
|
639
|
+
makeRequestSpy.mockResolvedValue(usage);
|
|
640
|
+
|
|
641
|
+
const result = await oxy.getAppUsage('app1', '7d');
|
|
642
|
+
|
|
643
|
+
expect(result).toEqual(usage);
|
|
644
|
+
expect(makeRequestSpy).toHaveBeenCalledWith(
|
|
645
|
+
'GET',
|
|
646
|
+
'/applications/app1/usage',
|
|
647
|
+
{ period: '7d' },
|
|
648
|
+
expect.objectContaining({ cache: true }),
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('omits the period query when not provided', async () => {
|
|
653
|
+
makeRequestSpy.mockResolvedValue({ summary: {}, byDay: [], byEndpoint: [] });
|
|
654
|
+
await oxy.getAppUsage('app1');
|
|
655
|
+
expect(makeRequestSpy.mock.calls[0][2]).toBeUndefined();
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
describe('error handling', () => {
|
|
660
|
+
it('surfaces API errors via handleError', async () => {
|
|
661
|
+
makeRequestSpy.mockRejectedValue(
|
|
662
|
+
Object.assign(new Error('boom'), { response: { status: 500 } }),
|
|
663
|
+
);
|
|
664
|
+
await expect(oxy.listAccounts()).rejects.toThrow();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { OxyServices } from '../../OxyServices';
|
|
12
|
-
import type { ConnectedApp } from '../OxyServices.
|
|
12
|
+
import type { ConnectedApp } from '../OxyServices.connectedApps';
|
|
13
13
|
|
|
14
14
|
/** Build a non-verified JWT whose payload decodes to the given claims. */
|
|
15
15
|
function makeJwt(payload: Record<string, unknown>): string {
|