@topolo/sdk 0.4.6 → 0.4.8
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/index.cjs +161 -144
- package/dist/index.d.cts +96 -104
- package/dist/index.d.ts +96 -104
- package/dist/index.js +161 -144
- package/package.json +1 -1
- package/src/application-requirements.ts +12 -6
- package/src/applications.generated.ts +61 -114
- package/src/client.test.ts +74 -0
- package/src/client.ts +101 -0
- package/src/index.test.ts +69 -0
- package/src/index.ts +1 -0
- package/src/modules/crm.test.ts +208 -0
- package/src/modules/identity.test.ts +165 -0
- package/src/modules/p2p.test.ts +286 -0
- package/src/oauth.test.ts +103 -0
- package/src/services.generated.ts +14 -15
- package/src/services.test.ts +19 -8
- package/src/widgets.test.ts +255 -1
- package/src/widgets.ts +5 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createTopolo,
|
|
4
|
+
CrmModule,
|
|
5
|
+
IdentityModule,
|
|
6
|
+
TopoloClient,
|
|
7
|
+
TopoloAuthError,
|
|
8
|
+
TopoloHttpError,
|
|
9
|
+
TopoloPermissionError,
|
|
10
|
+
} from './index.js';
|
|
11
|
+
|
|
12
|
+
const AGENT = { clientName: 'topolo-sdk-test', clientVersion: '0.0.0' };
|
|
13
|
+
|
|
14
|
+
describe('createTopolo', () => {
|
|
15
|
+
it('returns a ready client plus identity + crm module surfaces', () => {
|
|
16
|
+
const topolo = createTopolo({
|
|
17
|
+
credential: { kind: 'access_token', accessToken: 'tok' },
|
|
18
|
+
agent: AGENT,
|
|
19
|
+
fetch: vi.fn() as unknown as typeof fetch,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(topolo.client).toBeInstanceOf(TopoloClient);
|
|
23
|
+
expect(topolo.identity).toBeInstanceOf(IdentityModule);
|
|
24
|
+
expect(topolo.crm).toBeInstanceOf(CrmModule);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('wires every module to the same underlying client', async () => {
|
|
28
|
+
const calls: string[] = [];
|
|
29
|
+
const impl = vi.fn(async (url: string) => {
|
|
30
|
+
calls.push(String(url));
|
|
31
|
+
if (String(url).includes('/api/services/by-slug/')) {
|
|
32
|
+
return new Response(JSON.stringify({ success: true, data: { service_id: 'service_auth' } }), {
|
|
33
|
+
status: 200,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (String(url).includes('/api/services/catalog')) {
|
|
38
|
+
return new Response(JSON.stringify({ data: { services: [] } }), {
|
|
39
|
+
status: 200,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return new Response(
|
|
44
|
+
JSON.stringify({ data: { user: { id: 'u_1', email: 'a@b.c' } } }),
|
|
45
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
46
|
+
);
|
|
47
|
+
}) as unknown as typeof fetch;
|
|
48
|
+
|
|
49
|
+
TopoloClient.clearServiceIdCache();
|
|
50
|
+
const topolo = createTopolo({
|
|
51
|
+
credential: { kind: 'access_token', accessToken: 'tok' },
|
|
52
|
+
agent: AGENT,
|
|
53
|
+
fetch: impl,
|
|
54
|
+
serviceIds: { auth: 'service_auth' },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const me = await topolo.identity.whoami();
|
|
58
|
+
expect(me.user.id).toBe('u_1');
|
|
59
|
+
expect(calls.some((u) => u.includes('/api/auth/me'))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('package surface re-exports', () => {
|
|
64
|
+
it('re-exports the error classes from the barrel', () => {
|
|
65
|
+
expect(new TopoloAuthError('x').name).toBe('TopoloAuthError');
|
|
66
|
+
expect(new TopoloHttpError('auth', '/p', 500).name).toBe('TopoloHttpError');
|
|
67
|
+
expect(new TopoloPermissionError('nope', []).name).toBe('TopoloPermissionError');
|
|
68
|
+
});
|
|
69
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { TopoloClient } from '../client.js';
|
|
3
|
+
import { CrmModule } from './crm.js';
|
|
4
|
+
|
|
5
|
+
const AGENT = { clientName: 'topolo-sdk-test', clientVersion: '0.0.0' };
|
|
6
|
+
|
|
7
|
+
function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
|
|
8
|
+
const calls: Array<{ url: string; init: RequestInit }> = [];
|
|
9
|
+
const impl = vi.fn(async (url: string, init: RequestInit) => {
|
|
10
|
+
calls.push({ url, init });
|
|
11
|
+
if (String(url).includes('/api/services/by-slug/')) {
|
|
12
|
+
return new Response(JSON.stringify({ success: true, data: { service_id: 'service_crm' } }), {
|
|
13
|
+
status: 200,
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (String(url).includes('/api/services/catalog')) {
|
|
18
|
+
return new Response(
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
data: {
|
|
21
|
+
services: [
|
|
22
|
+
{
|
|
23
|
+
service_id: 'srv_crm',
|
|
24
|
+
app_slug: 'topolo-crm',
|
|
25
|
+
name: 'Topolo CRM',
|
|
26
|
+
api_base_url: 'https://crm.topolo.app',
|
|
27
|
+
launch_url: 'https://crm.topolo.app',
|
|
28
|
+
surface_kind: 'api',
|
|
29
|
+
status: 'active',
|
|
30
|
+
granted_permissions: ['srv_crm.contacts:read'],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const next = responses.shift() ?? { status: 200, body: { data: {} } };
|
|
39
|
+
return new Response(JSON.stringify(next.body ?? {}), {
|
|
40
|
+
status: next.status ?? 200,
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
return { impl: impl as unknown as typeof fetch, calls };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requestCalls(calls: Array<{ url: string; init: RequestInit }>) {
|
|
48
|
+
return calls.filter(
|
|
49
|
+
(call) =>
|
|
50
|
+
!call.url.includes('/api/services/by-slug/') &&
|
|
51
|
+
!call.url.includes('/api/services/catalog'),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function clientWith(impl: typeof fetch) {
|
|
56
|
+
return new TopoloClient({
|
|
57
|
+
credential: { kind: 'access_token', accessToken: 'tok_test' },
|
|
58
|
+
agent: AGENT,
|
|
59
|
+
fetch: impl,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
TopoloClient.clearServiceIdCache();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('CrmModule.listContacts', () => {
|
|
68
|
+
it('maps snake_case rows into camelCase summaries and forwards query params', async () => {
|
|
69
|
+
const { impl, calls } = stubFetch([
|
|
70
|
+
{
|
|
71
|
+
body: {
|
|
72
|
+
contacts: [
|
|
73
|
+
{
|
|
74
|
+
id: 'c_1',
|
|
75
|
+
first: 'Ada',
|
|
76
|
+
last: 'Lovelace',
|
|
77
|
+
email: 'ada@example.com',
|
|
78
|
+
phone: '+1',
|
|
79
|
+
company: 'Analytical',
|
|
80
|
+
lead_status: 'new',
|
|
81
|
+
lifecycle_stage: 'lead',
|
|
82
|
+
updated_at: '2026-01-01T00:00:00.000Z',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
total: 42,
|
|
86
|
+
page: 2,
|
|
87
|
+
pageSize: 25,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
const crm = new CrmModule(clientWith(impl));
|
|
92
|
+
|
|
93
|
+
const result = await crm.listContacts({ q: 'ada', page: 2, pageSize: 25 });
|
|
94
|
+
|
|
95
|
+
expect(result.contacts).toEqual([
|
|
96
|
+
{
|
|
97
|
+
id: 'c_1',
|
|
98
|
+
firstName: 'Ada',
|
|
99
|
+
lastName: 'Lovelace',
|
|
100
|
+
email: 'ada@example.com',
|
|
101
|
+
phone: '+1',
|
|
102
|
+
company: 'Analytical',
|
|
103
|
+
leadStatus: 'new',
|
|
104
|
+
lifecycleStage: 'lead',
|
|
105
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
expect(result.total).toBe(42);
|
|
109
|
+
expect(result.page).toBe(2);
|
|
110
|
+
expect(result.pageSize).toBe(25);
|
|
111
|
+
|
|
112
|
+
const url = new URL(requestCalls(calls)[0]!.url);
|
|
113
|
+
expect(url.pathname).toBe('/api/contacts');
|
|
114
|
+
expect(url.searchParams.get('q')).toBe('ada');
|
|
115
|
+
expect(url.searchParams.get('page')).toBe('2');
|
|
116
|
+
expect(url.searchParams.get('pageSize')).toBe('25');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('reads rows from the data[] envelope and pagination block', async () => {
|
|
120
|
+
const { impl } = stubFetch([
|
|
121
|
+
{
|
|
122
|
+
body: {
|
|
123
|
+
data: [{ id: 'c_2' }],
|
|
124
|
+
pagination: { total: 7, page: 3, pageSize: 10 },
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
const crm = new CrmModule(clientWith(impl));
|
|
129
|
+
|
|
130
|
+
const result = await crm.listContacts();
|
|
131
|
+
|
|
132
|
+
expect(result.contacts).toEqual([
|
|
133
|
+
{
|
|
134
|
+
id: 'c_2',
|
|
135
|
+
firstName: null,
|
|
136
|
+
lastName: null,
|
|
137
|
+
email: null,
|
|
138
|
+
phone: null,
|
|
139
|
+
company: null,
|
|
140
|
+
leadStatus: null,
|
|
141
|
+
lifecycleStage: null,
|
|
142
|
+
updatedAt: null,
|
|
143
|
+
},
|
|
144
|
+
]);
|
|
145
|
+
expect(result.total).toBe(7);
|
|
146
|
+
expect(result.page).toBe(3);
|
|
147
|
+
expect(result.pageSize).toBe(10);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('falls back to row count + option defaults when no metadata is returned', async () => {
|
|
151
|
+
const { impl } = stubFetch([{ body: { contacts: [{ id: 'c_3' }, { id: 'c_4' }] } }]);
|
|
152
|
+
const crm = new CrmModule(clientWith(impl));
|
|
153
|
+
|
|
154
|
+
const result = await crm.listContacts();
|
|
155
|
+
|
|
156
|
+
expect(result.total).toBe(2);
|
|
157
|
+
expect(result.page).toBe(1);
|
|
158
|
+
expect(result.pageSize).toBe(2);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('returns an empty list when the response has neither contacts nor data arrays', async () => {
|
|
162
|
+
const { impl } = stubFetch([{ body: { unexpected: true } }]);
|
|
163
|
+
const crm = new CrmModule(clientWith(impl));
|
|
164
|
+
|
|
165
|
+
const result = await crm.listContacts();
|
|
166
|
+
|
|
167
|
+
expect(result.contacts).toEqual([]);
|
|
168
|
+
expect(result.total).toBe(0);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('CrmModule.getContact', () => {
|
|
173
|
+
it('rejects an empty contactId', async () => {
|
|
174
|
+
const { impl } = stubFetch([]);
|
|
175
|
+
const crm = new CrmModule(clientWith(impl));
|
|
176
|
+
await expect(crm.getContact('')).rejects.toThrow('contactId is required');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('unwraps a data-enveloped contact', async () => {
|
|
180
|
+
const { impl, calls } = stubFetch([
|
|
181
|
+
{ body: { data: { id: 'c_9', first: 'Grace', last: 'Hopper' } } },
|
|
182
|
+
]);
|
|
183
|
+
const crm = new CrmModule(clientWith(impl));
|
|
184
|
+
|
|
185
|
+
const contact = await crm.getContact('c_9');
|
|
186
|
+
|
|
187
|
+
expect(contact).toMatchObject({ id: 'c_9', firstName: 'Grace', lastName: 'Hopper' });
|
|
188
|
+
expect(new URL(requestCalls(calls)[0]!.url).pathname).toBe('/api/contacts/c_9');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('accepts a bare contact object (no data envelope)', async () => {
|
|
192
|
+
const { impl } = stubFetch([{ body: { id: 'c_10', email: 'g@example.com' } }]);
|
|
193
|
+
const crm = new CrmModule(clientWith(impl));
|
|
194
|
+
|
|
195
|
+
const contact = await crm.getContact('c_10');
|
|
196
|
+
|
|
197
|
+
expect(contact).toMatchObject({ id: 'c_10', email: 'g@example.com' });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('url-encodes contact ids with special characters', async () => {
|
|
201
|
+
const { impl, calls } = stubFetch([{ body: { data: { id: 'a/b c' } } }]);
|
|
202
|
+
const crm = new CrmModule(clientWith(impl));
|
|
203
|
+
|
|
204
|
+
await crm.getContact('a/b c');
|
|
205
|
+
|
|
206
|
+
expect(requestCalls(calls)[0]!.url).toContain('/api/contacts/a%2Fb%20c');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { TopoloClient } from '../client.js';
|
|
3
|
+
import { IdentityModule } from './identity.js';
|
|
4
|
+
|
|
5
|
+
const AGENT = { clientName: 'topolo-sdk-test', clientVersion: '0.0.0' };
|
|
6
|
+
|
|
7
|
+
function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
|
|
8
|
+
const calls: Array<{ url: string; init: RequestInit }> = [];
|
|
9
|
+
const impl = vi.fn(async (url: string, init: RequestInit) => {
|
|
10
|
+
calls.push({ url, init });
|
|
11
|
+
if (String(url).includes('/api/services/by-slug/')) {
|
|
12
|
+
return new Response(JSON.stringify({ success: true, data: { service_id: 'service_auth' } }), {
|
|
13
|
+
status: 200,
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (String(url).includes('/api/services/catalog')) {
|
|
18
|
+
return new Response(JSON.stringify({ data: { services: [] } }), {
|
|
19
|
+
status: 200,
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const next = responses.shift() ?? { status: 200, body: {} };
|
|
24
|
+
return new Response(JSON.stringify(next.body ?? {}), {
|
|
25
|
+
status: next.status ?? 200,
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
return { impl: impl as unknown as typeof fetch, calls };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function requestCalls(calls: Array<{ url: string; init: RequestInit }>) {
|
|
33
|
+
return calls.filter(
|
|
34
|
+
(call) =>
|
|
35
|
+
!call.url.includes('/api/services/by-slug/') &&
|
|
36
|
+
!call.url.includes('/api/services/catalog'),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clientWith(
|
|
41
|
+
impl: typeof fetch,
|
|
42
|
+
credential: { kind: 'access_token'; accessToken: string } | { kind: 'api_key'; apiKey: string } = {
|
|
43
|
+
kind: 'access_token',
|
|
44
|
+
accessToken: 'tok_test',
|
|
45
|
+
},
|
|
46
|
+
) {
|
|
47
|
+
return new TopoloClient({
|
|
48
|
+
credential,
|
|
49
|
+
agent: AGENT,
|
|
50
|
+
fetch: impl,
|
|
51
|
+
serviceIds: { auth: 'service_auth' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
TopoloClient.clearServiceIdCache();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('IdentityModule.whoami', () => {
|
|
60
|
+
it('resolves a data-enveloped access-token identity with an organization', async () => {
|
|
61
|
+
const { impl, calls } = stubFetch([
|
|
62
|
+
{
|
|
63
|
+
body: {
|
|
64
|
+
data: {
|
|
65
|
+
credentialType: 'access_token',
|
|
66
|
+
user: { id: 'u_1', email: 'ada@example.com', name: 'Ada', role: 'owner' },
|
|
67
|
+
organization: { id: 'org_1', slug: 'acme', name: 'Acme Inc' },
|
|
68
|
+
permissions: ['crm.contacts:read'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
]);
|
|
73
|
+
const identity = new IdentityModule(clientWith(impl));
|
|
74
|
+
|
|
75
|
+
const me = await identity.whoami();
|
|
76
|
+
|
|
77
|
+
expect(me).toEqual({
|
|
78
|
+
kind: 'access_token',
|
|
79
|
+
user: {
|
|
80
|
+
id: 'u_1',
|
|
81
|
+
email: 'ada@example.com',
|
|
82
|
+
name: 'Ada',
|
|
83
|
+
role: 'owner',
|
|
84
|
+
permissions: ['crm.contacts:read'],
|
|
85
|
+
},
|
|
86
|
+
organization: { id: 'org_1', slug: 'acme', name: 'Acme Inc' },
|
|
87
|
+
});
|
|
88
|
+
expect(new URL(requestCalls(calls)[0]!.url).pathname).toBe('/api/auth/me');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('defaults api_key role to "service" and reads a bare (non-enveloped) payload', async () => {
|
|
92
|
+
const { impl } = stubFetch([
|
|
93
|
+
{
|
|
94
|
+
body: {
|
|
95
|
+
credentialType: 'api_key',
|
|
96
|
+
user: { id: 'svc_1', email: 'svc@example.com' },
|
|
97
|
+
organization: { id: 'org_2', slug: 'beta' },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
const identity = new IdentityModule(
|
|
102
|
+
clientWith(impl, { kind: 'api_key', apiKey: 'dak_xyz' }),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const me = await identity.whoami();
|
|
106
|
+
|
|
107
|
+
expect(me.kind).toBe('api_key');
|
|
108
|
+
expect(me.user.role).toBe('service');
|
|
109
|
+
expect(me.user.name).toBeNull();
|
|
110
|
+
// organization name falls back to slug when name is absent
|
|
111
|
+
expect(me.organization).toEqual({ id: 'org_2', slug: 'beta', name: 'beta' });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('defaults access_token role to "member" and reads permissions from user when top-level absent', async () => {
|
|
115
|
+
const { impl } = stubFetch([
|
|
116
|
+
{
|
|
117
|
+
body: {
|
|
118
|
+
data: {
|
|
119
|
+
user: {
|
|
120
|
+
id: 'u_3',
|
|
121
|
+
email: 'm@example.com',
|
|
122
|
+
permissions: ['mail.messages:read'],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
const identity = new IdentityModule(clientWith(impl));
|
|
129
|
+
|
|
130
|
+
const me = await identity.whoami();
|
|
131
|
+
|
|
132
|
+
expect(me.kind).toBe('access_token');
|
|
133
|
+
expect(me.user.role).toBe('member');
|
|
134
|
+
expect(me.user.permissions).toEqual(['mail.messages:read']);
|
|
135
|
+
expect(me.organization).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('synthesizes an organization from user.orgId/orgSlug when no organization object is present', async () => {
|
|
139
|
+
const { impl } = stubFetch([
|
|
140
|
+
{
|
|
141
|
+
body: {
|
|
142
|
+
data: {
|
|
143
|
+
user: { id: 'u_4', email: 'o@example.com', orgId: 'org_9', orgSlug: 'gamma' },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
const identity = new IdentityModule(clientWith(impl));
|
|
149
|
+
|
|
150
|
+
const me = await identity.whoami();
|
|
151
|
+
|
|
152
|
+
expect(me.organization).toEqual({ id: 'org_9', slug: 'gamma', name: 'gamma' });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('defaults missing permissions to an empty array', async () => {
|
|
156
|
+
const { impl } = stubFetch([
|
|
157
|
+
{ body: { data: { user: { id: 'u_5', email: 'n@example.com' } } } },
|
|
158
|
+
]);
|
|
159
|
+
const identity = new IdentityModule(clientWith(impl));
|
|
160
|
+
|
|
161
|
+
const me = await identity.whoami();
|
|
162
|
+
|
|
163
|
+
expect(me.user.permissions).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
});
|