@vibescope/mcp-server 0.0.1
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/README.md +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
listOrganizations,
|
|
4
|
+
createOrganization,
|
|
5
|
+
updateOrganization,
|
|
6
|
+
deleteOrganization,
|
|
7
|
+
listOrgMembers,
|
|
8
|
+
inviteMember,
|
|
9
|
+
updateMemberRole,
|
|
10
|
+
removeMember,
|
|
11
|
+
leaveOrganization,
|
|
12
|
+
shareProjectWithOrg,
|
|
13
|
+
updateProjectShare,
|
|
14
|
+
unshareProject,
|
|
15
|
+
listProjectShares,
|
|
16
|
+
} from './organizations.js';
|
|
17
|
+
import { ValidationError } from '../validators.js';
|
|
18
|
+
import { createMockSupabase, createMockContext, testUUID } from './__test-utils__.js';
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// listOrganizations Tests
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
describe('listOrganizations', () => {
|
|
25
|
+
beforeEach(() => vi.clearAllMocks());
|
|
26
|
+
|
|
27
|
+
it('should return empty array when no organizations', async () => {
|
|
28
|
+
const supabase = createMockSupabase({
|
|
29
|
+
selectResult: { data: [], error: null },
|
|
30
|
+
});
|
|
31
|
+
const ctx = createMockContext(supabase);
|
|
32
|
+
|
|
33
|
+
const result = await listOrganizations({}, ctx);
|
|
34
|
+
|
|
35
|
+
expect(result.result).toMatchObject({
|
|
36
|
+
organizations: [],
|
|
37
|
+
count: 0,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return organizations with role and joined_at', async () => {
|
|
42
|
+
const supabase = createMockSupabase({
|
|
43
|
+
selectResult: {
|
|
44
|
+
data: [
|
|
45
|
+
{
|
|
46
|
+
role: 'admin',
|
|
47
|
+
joined_at: '2025-01-14T12:00:00Z',
|
|
48
|
+
organizations: {
|
|
49
|
+
id: 'org-1',
|
|
50
|
+
name: 'Test Org',
|
|
51
|
+
slug: 'test-org',
|
|
52
|
+
description: 'A test organization',
|
|
53
|
+
logo_url: null,
|
|
54
|
+
owner_id: 'user-123',
|
|
55
|
+
created_at: '2025-01-01T00:00:00Z',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
error: null,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const ctx = createMockContext(supabase);
|
|
63
|
+
|
|
64
|
+
const result = await listOrganizations({}, ctx);
|
|
65
|
+
|
|
66
|
+
expect(result.result).toMatchObject({
|
|
67
|
+
count: 1,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should throw error when query fails', async () => {
|
|
72
|
+
const supabase = createMockSupabase({
|
|
73
|
+
selectResult: { data: null, error: { message: 'Query failed' } },
|
|
74
|
+
});
|
|
75
|
+
const ctx = createMockContext(supabase);
|
|
76
|
+
|
|
77
|
+
await expect(listOrganizations({}, ctx)).rejects.toThrow(
|
|
78
|
+
'Failed to list organizations: Query failed'
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// createOrganization Tests
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
describe('createOrganization', () => {
|
|
88
|
+
beforeEach(() => vi.clearAllMocks());
|
|
89
|
+
|
|
90
|
+
it('should throw error for missing name', async () => {
|
|
91
|
+
const supabase = createMockSupabase();
|
|
92
|
+
const ctx = createMockContext(supabase);
|
|
93
|
+
|
|
94
|
+
await expect(createOrganization({}, ctx)).rejects.toThrow(ValidationError);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should throw error when slug is taken', async () => {
|
|
98
|
+
const supabase = createMockSupabase({
|
|
99
|
+
selectResult: { data: { id: 'existing-org' }, error: null },
|
|
100
|
+
});
|
|
101
|
+
const ctx = createMockContext(supabase);
|
|
102
|
+
|
|
103
|
+
await expect(
|
|
104
|
+
createOrganization({ name: 'Test Org' }, ctx)
|
|
105
|
+
).rejects.toThrow('Organization slug "test-org" is already taken');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should create organization with auto-generated slug', async () => {
|
|
109
|
+
const supabase = createMockSupabase({
|
|
110
|
+
selectResult: { data: null, error: null }, // No existing slug
|
|
111
|
+
insertResult: {
|
|
112
|
+
data: {
|
|
113
|
+
id: 'org-1',
|
|
114
|
+
name: 'Test Org',
|
|
115
|
+
slug: 'test-org',
|
|
116
|
+
owner_id: 'user-123',
|
|
117
|
+
},
|
|
118
|
+
error: null,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
const ctx = createMockContext(supabase);
|
|
122
|
+
|
|
123
|
+
const result = await createOrganization({ name: 'Test Org' }, ctx);
|
|
124
|
+
|
|
125
|
+
expect(result.result).toMatchObject({
|
|
126
|
+
success: true,
|
|
127
|
+
message: 'Organization "Test Org" created. You are the owner.',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should create organization with custom slug', async () => {
|
|
132
|
+
const supabase = createMockSupabase({
|
|
133
|
+
selectResult: { data: null, error: null },
|
|
134
|
+
insertResult: {
|
|
135
|
+
data: {
|
|
136
|
+
id: 'org-1',
|
|
137
|
+
name: 'Test Org',
|
|
138
|
+
slug: 'my-custom-slug',
|
|
139
|
+
owner_id: 'user-123',
|
|
140
|
+
},
|
|
141
|
+
error: null,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const ctx = createMockContext(supabase);
|
|
145
|
+
|
|
146
|
+
const result = await createOrganization(
|
|
147
|
+
{ name: 'Test Org', slug: 'my-custom-slug' },
|
|
148
|
+
ctx
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(result.result).toMatchObject({
|
|
152
|
+
success: true,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should throw error when insert fails', async () => {
|
|
157
|
+
const supabase = createMockSupabase({
|
|
158
|
+
selectResult: { data: null, error: null },
|
|
159
|
+
insertResult: { data: null, error: { message: 'Insert failed' } },
|
|
160
|
+
});
|
|
161
|
+
const ctx = createMockContext(supabase);
|
|
162
|
+
|
|
163
|
+
await expect(
|
|
164
|
+
createOrganization({ name: 'Test Org' }, ctx)
|
|
165
|
+
).rejects.toThrow('Failed to create organization: Insert failed');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// updateOrganization Tests
|
|
171
|
+
// ============================================================================
|
|
172
|
+
|
|
173
|
+
describe('updateOrganization', () => {
|
|
174
|
+
beforeEach(() => vi.clearAllMocks());
|
|
175
|
+
|
|
176
|
+
it('should throw error for missing organization_id', async () => {
|
|
177
|
+
const supabase = createMockSupabase();
|
|
178
|
+
const ctx = createMockContext(supabase);
|
|
179
|
+
|
|
180
|
+
await expect(
|
|
181
|
+
updateOrganization({ name: 'New Name' }, ctx)
|
|
182
|
+
).rejects.toThrow(ValidationError);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should throw error for invalid organization_id UUID', async () => {
|
|
186
|
+
const supabase = createMockSupabase();
|
|
187
|
+
const ctx = createMockContext(supabase);
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
updateOrganization({ organization_id: 'invalid', name: 'New Name' }, ctx)
|
|
191
|
+
).rejects.toThrow(ValidationError);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should throw error when no updates provided', async () => {
|
|
195
|
+
const supabase = createMockSupabase();
|
|
196
|
+
const ctx = createMockContext(supabase);
|
|
197
|
+
|
|
198
|
+
await expect(
|
|
199
|
+
updateOrganization({ organization_id: testUUID() }, ctx)
|
|
200
|
+
).rejects.toThrow('No updates provided');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should update organization successfully', async () => {
|
|
204
|
+
const supabase = createMockSupabase({
|
|
205
|
+
updateResult: {
|
|
206
|
+
data: { id: testUUID(), name: 'Updated Org', description: 'New desc' },
|
|
207
|
+
error: null,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const ctx = createMockContext(supabase);
|
|
211
|
+
|
|
212
|
+
const result = await updateOrganization(
|
|
213
|
+
{
|
|
214
|
+
organization_id: testUUID(),
|
|
215
|
+
name: 'Updated Org',
|
|
216
|
+
description: 'New desc',
|
|
217
|
+
},
|
|
218
|
+
ctx
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
expect(result.result).toMatchObject({
|
|
222
|
+
success: true,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should throw error when update fails', async () => {
|
|
227
|
+
const supabase = createMockSupabase({
|
|
228
|
+
updateResult: { data: null, error: { message: 'Update failed' } },
|
|
229
|
+
});
|
|
230
|
+
const ctx = createMockContext(supabase);
|
|
231
|
+
|
|
232
|
+
await expect(
|
|
233
|
+
updateOrganization({ organization_id: testUUID(), name: 'New Name' }, ctx)
|
|
234
|
+
).rejects.toThrow('Failed to update organization: Update failed');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// deleteOrganization Tests
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
describe('deleteOrganization', () => {
|
|
243
|
+
beforeEach(() => vi.clearAllMocks());
|
|
244
|
+
|
|
245
|
+
it('should throw error for missing organization_id', async () => {
|
|
246
|
+
const supabase = createMockSupabase();
|
|
247
|
+
const ctx = createMockContext(supabase);
|
|
248
|
+
|
|
249
|
+
await expect(deleteOrganization({}, ctx)).rejects.toThrow(ValidationError);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should throw error for invalid organization_id UUID', async () => {
|
|
253
|
+
const supabase = createMockSupabase();
|
|
254
|
+
const ctx = createMockContext(supabase);
|
|
255
|
+
|
|
256
|
+
await expect(
|
|
257
|
+
deleteOrganization({ organization_id: 'invalid' }, ctx)
|
|
258
|
+
).rejects.toThrow(ValidationError);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should delete organization successfully', async () => {
|
|
262
|
+
const supabase = createMockSupabase({
|
|
263
|
+
deleteResult: { data: null, error: null },
|
|
264
|
+
});
|
|
265
|
+
const ctx = createMockContext(supabase);
|
|
266
|
+
|
|
267
|
+
const result = await deleteOrganization({ organization_id: testUUID() }, ctx);
|
|
268
|
+
|
|
269
|
+
expect(result.result).toMatchObject({
|
|
270
|
+
success: true,
|
|
271
|
+
message: 'Organization deleted. All shares have been removed.',
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should throw error when delete fails', async () => {
|
|
276
|
+
const supabase = createMockSupabase({
|
|
277
|
+
deleteResult: { data: null, error: { message: 'Delete failed' } },
|
|
278
|
+
});
|
|
279
|
+
const ctx = createMockContext(supabase);
|
|
280
|
+
|
|
281
|
+
await expect(
|
|
282
|
+
deleteOrganization({ organization_id: testUUID() }, ctx)
|
|
283
|
+
).rejects.toThrow('Failed to delete organization: Delete failed');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// listOrgMembers Tests
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
describe('listOrgMembers', () => {
|
|
292
|
+
beforeEach(() => vi.clearAllMocks());
|
|
293
|
+
|
|
294
|
+
it('should throw error for missing organization_id', async () => {
|
|
295
|
+
const supabase = createMockSupabase();
|
|
296
|
+
const ctx = createMockContext(supabase);
|
|
297
|
+
|
|
298
|
+
await expect(listOrgMembers({}, ctx)).rejects.toThrow(ValidationError);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should throw error for invalid organization_id UUID', async () => {
|
|
302
|
+
const supabase = createMockSupabase();
|
|
303
|
+
const ctx = createMockContext(supabase);
|
|
304
|
+
|
|
305
|
+
await expect(
|
|
306
|
+
listOrgMembers({ organization_id: 'invalid' }, ctx)
|
|
307
|
+
).rejects.toThrow(ValidationError);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should return empty array when no members', async () => {
|
|
311
|
+
const supabase = createMockSupabase({
|
|
312
|
+
selectResult: { data: [], error: null },
|
|
313
|
+
});
|
|
314
|
+
const ctx = createMockContext(supabase);
|
|
315
|
+
|
|
316
|
+
const result = await listOrgMembers({ organization_id: testUUID() }, ctx);
|
|
317
|
+
|
|
318
|
+
expect(result.result).toMatchObject({
|
|
319
|
+
members: [],
|
|
320
|
+
count: 0,
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should return members list', async () => {
|
|
325
|
+
const supabase = createMockSupabase({
|
|
326
|
+
selectResult: {
|
|
327
|
+
data: [
|
|
328
|
+
{ id: 'm-1', user_id: 'u-1', role: 'owner', joined_at: '2025-01-01T00:00:00Z' },
|
|
329
|
+
{ id: 'm-2', user_id: 'u-2', role: 'member', joined_at: '2025-01-02T00:00:00Z' },
|
|
330
|
+
],
|
|
331
|
+
error: null,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
const ctx = createMockContext(supabase);
|
|
335
|
+
|
|
336
|
+
const result = await listOrgMembers({ organization_id: testUUID() }, ctx);
|
|
337
|
+
|
|
338
|
+
expect(result.result).toMatchObject({
|
|
339
|
+
count: 2,
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should throw error when query fails', async () => {
|
|
344
|
+
const supabase = createMockSupabase({
|
|
345
|
+
selectResult: { data: null, error: { message: 'Query failed' } },
|
|
346
|
+
});
|
|
347
|
+
const ctx = createMockContext(supabase);
|
|
348
|
+
|
|
349
|
+
await expect(
|
|
350
|
+
listOrgMembers({ organization_id: testUUID() }, ctx)
|
|
351
|
+
).rejects.toThrow('Failed to list members: Query failed');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ============================================================================
|
|
356
|
+
// inviteMember Tests
|
|
357
|
+
// ============================================================================
|
|
358
|
+
|
|
359
|
+
describe('inviteMember', () => {
|
|
360
|
+
beforeEach(() => vi.clearAllMocks());
|
|
361
|
+
|
|
362
|
+
it('should throw error for missing organization_id', async () => {
|
|
363
|
+
const supabase = createMockSupabase();
|
|
364
|
+
const ctx = createMockContext(supabase);
|
|
365
|
+
|
|
366
|
+
await expect(
|
|
367
|
+
inviteMember({ email: 'test@example.com' }, ctx)
|
|
368
|
+
).rejects.toThrow(ValidationError);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should throw error for missing email', async () => {
|
|
372
|
+
const supabase = createMockSupabase();
|
|
373
|
+
const ctx = createMockContext(supabase);
|
|
374
|
+
|
|
375
|
+
await expect(
|
|
376
|
+
inviteMember({ organization_id: testUUID() }, ctx)
|
|
377
|
+
).rejects.toThrow(ValidationError);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should throw error for invalid role', async () => {
|
|
381
|
+
const supabase = createMockSupabase({
|
|
382
|
+
selectResult: { data: null, error: null }, // No existing invite
|
|
383
|
+
});
|
|
384
|
+
const ctx = createMockContext(supabase);
|
|
385
|
+
|
|
386
|
+
await expect(
|
|
387
|
+
inviteMember({
|
|
388
|
+
organization_id: testUUID(),
|
|
389
|
+
email: 'test@example.com',
|
|
390
|
+
role: 'owner', // Invalid - can't invite as owner
|
|
391
|
+
}, ctx)
|
|
392
|
+
).rejects.toThrow('Invalid role. Must be admin, member, or viewer.');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should throw error when invite already exists', async () => {
|
|
396
|
+
const supabase = createMockSupabase({
|
|
397
|
+
selectResult: { data: { id: 'existing-invite' }, error: null },
|
|
398
|
+
});
|
|
399
|
+
const ctx = createMockContext(supabase);
|
|
400
|
+
|
|
401
|
+
await expect(
|
|
402
|
+
inviteMember({
|
|
403
|
+
organization_id: testUUID(),
|
|
404
|
+
email: 'test@example.com',
|
|
405
|
+
}, ctx)
|
|
406
|
+
).rejects.toThrow('A pending invite already exists for test@example.com');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should create invite successfully with default role', async () => {
|
|
410
|
+
const supabase = createMockSupabase({
|
|
411
|
+
selectResult: { data: null, error: null }, // No existing invite
|
|
412
|
+
insertResult: {
|
|
413
|
+
data: {
|
|
414
|
+
id: 'invite-1',
|
|
415
|
+
email: 'test@example.com',
|
|
416
|
+
role: 'member',
|
|
417
|
+
token: 'abc123',
|
|
418
|
+
},
|
|
419
|
+
error: null,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
const ctx = createMockContext(supabase);
|
|
423
|
+
|
|
424
|
+
const result = await inviteMember({
|
|
425
|
+
organization_id: testUUID(),
|
|
426
|
+
email: 'test@example.com',
|
|
427
|
+
}, ctx);
|
|
428
|
+
|
|
429
|
+
expect(result.result).toMatchObject({
|
|
430
|
+
success: true,
|
|
431
|
+
message: 'Invite sent to test@example.com with role "member"',
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should throw error when insert fails', async () => {
|
|
436
|
+
const supabase = createMockSupabase({
|
|
437
|
+
selectResult: { data: null, error: null },
|
|
438
|
+
insertResult: { data: null, error: { message: 'Insert failed' } },
|
|
439
|
+
});
|
|
440
|
+
const ctx = createMockContext(supabase);
|
|
441
|
+
|
|
442
|
+
await expect(
|
|
443
|
+
inviteMember({
|
|
444
|
+
organization_id: testUUID(),
|
|
445
|
+
email: 'test@example.com',
|
|
446
|
+
}, ctx)
|
|
447
|
+
).rejects.toThrow('Failed to create invite: Insert failed');
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// ============================================================================
|
|
452
|
+
// updateMemberRole Tests
|
|
453
|
+
// ============================================================================
|
|
454
|
+
|
|
455
|
+
describe('updateMemberRole', () => {
|
|
456
|
+
beforeEach(() => vi.clearAllMocks());
|
|
457
|
+
|
|
458
|
+
it('should throw error for missing organization_id', async () => {
|
|
459
|
+
const supabase = createMockSupabase();
|
|
460
|
+
const ctx = createMockContext(supabase);
|
|
461
|
+
|
|
462
|
+
await expect(
|
|
463
|
+
updateMemberRole({ user_id: testUUID(), role: 'admin' }, ctx)
|
|
464
|
+
).rejects.toThrow(ValidationError);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should throw error for missing user_id', async () => {
|
|
468
|
+
const supabase = createMockSupabase();
|
|
469
|
+
const ctx = createMockContext(supabase);
|
|
470
|
+
|
|
471
|
+
await expect(
|
|
472
|
+
updateMemberRole({ organization_id: testUUID(), role: 'admin' }, ctx)
|
|
473
|
+
).rejects.toThrow(ValidationError);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should throw error for missing role', async () => {
|
|
477
|
+
const supabase = createMockSupabase();
|
|
478
|
+
const ctx = createMockContext(supabase);
|
|
479
|
+
|
|
480
|
+
await expect(
|
|
481
|
+
updateMemberRole({ organization_id: testUUID(), user_id: testUUID() }, ctx)
|
|
482
|
+
).rejects.toThrow(ValidationError);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('should throw error for invalid role', async () => {
|
|
486
|
+
const supabase = createMockSupabase();
|
|
487
|
+
const ctx = createMockContext(supabase);
|
|
488
|
+
|
|
489
|
+
await expect(
|
|
490
|
+
updateMemberRole({
|
|
491
|
+
organization_id: testUUID(),
|
|
492
|
+
user_id: testUUID(),
|
|
493
|
+
role: 'superadmin',
|
|
494
|
+
}, ctx)
|
|
495
|
+
).rejects.toThrow('Invalid role. Must be one of: viewer, member, admin, owner');
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should throw error when trying to assign owner role', async () => {
|
|
499
|
+
const supabase = createMockSupabase();
|
|
500
|
+
const ctx = createMockContext(supabase);
|
|
501
|
+
|
|
502
|
+
await expect(
|
|
503
|
+
updateMemberRole({
|
|
504
|
+
organization_id: testUUID(),
|
|
505
|
+
user_id: testUUID(),
|
|
506
|
+
role: 'owner',
|
|
507
|
+
}, ctx)
|
|
508
|
+
).rejects.toThrow('Cannot assign owner role. Use transfer ownership instead.');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('should throw error when changing own role', async () => {
|
|
512
|
+
const supabase = createMockSupabase();
|
|
513
|
+
const ownUserId = testUUID(); // Use valid UUID
|
|
514
|
+
const ctx = createMockContext(supabase, { userId: ownUserId });
|
|
515
|
+
|
|
516
|
+
await expect(
|
|
517
|
+
updateMemberRole({
|
|
518
|
+
organization_id: testUUID(),
|
|
519
|
+
user_id: ownUserId,
|
|
520
|
+
role: 'admin',
|
|
521
|
+
}, ctx)
|
|
522
|
+
).rejects.toThrow('Cannot change your own role');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should update member role successfully', async () => {
|
|
526
|
+
const supabase = createMockSupabase({
|
|
527
|
+
updateResult: {
|
|
528
|
+
data: { user_id: testUUID(), role: 'admin' },
|
|
529
|
+
error: null,
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
const ctx = createMockContext(supabase);
|
|
533
|
+
|
|
534
|
+
const result = await updateMemberRole({
|
|
535
|
+
organization_id: testUUID(),
|
|
536
|
+
user_id: testUUID(),
|
|
537
|
+
role: 'admin',
|
|
538
|
+
}, ctx);
|
|
539
|
+
|
|
540
|
+
expect(result.result).toMatchObject({
|
|
541
|
+
success: true,
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should throw error when update fails', async () => {
|
|
546
|
+
const supabase = createMockSupabase({
|
|
547
|
+
updateResult: { data: null, error: { message: 'Update failed' } },
|
|
548
|
+
});
|
|
549
|
+
const ctx = createMockContext(supabase);
|
|
550
|
+
|
|
551
|
+
await expect(
|
|
552
|
+
updateMemberRole({
|
|
553
|
+
organization_id: testUUID(),
|
|
554
|
+
user_id: testUUID(),
|
|
555
|
+
role: 'admin',
|
|
556
|
+
}, ctx)
|
|
557
|
+
).rejects.toThrow('Failed to update member role: Update failed');
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// ============================================================================
|
|
562
|
+
// removeMember Tests
|
|
563
|
+
// ============================================================================
|
|
564
|
+
|
|
565
|
+
describe('removeMember', () => {
|
|
566
|
+
beforeEach(() => vi.clearAllMocks());
|
|
567
|
+
|
|
568
|
+
it('should throw error for missing organization_id', async () => {
|
|
569
|
+
const supabase = createMockSupabase();
|
|
570
|
+
const ctx = createMockContext(supabase);
|
|
571
|
+
|
|
572
|
+
await expect(
|
|
573
|
+
removeMember({ user_id: testUUID() }, ctx)
|
|
574
|
+
).rejects.toThrow(ValidationError);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should throw error for missing user_id', async () => {
|
|
578
|
+
const supabase = createMockSupabase();
|
|
579
|
+
const ctx = createMockContext(supabase);
|
|
580
|
+
|
|
581
|
+
await expect(
|
|
582
|
+
removeMember({ organization_id: testUUID() }, ctx)
|
|
583
|
+
).rejects.toThrow(ValidationError);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('should remove member successfully', async () => {
|
|
587
|
+
const supabase = createMockSupabase({
|
|
588
|
+
deleteResult: { data: null, error: null },
|
|
589
|
+
});
|
|
590
|
+
const ctx = createMockContext(supabase);
|
|
591
|
+
|
|
592
|
+
const result = await removeMember({
|
|
593
|
+
organization_id: testUUID(),
|
|
594
|
+
user_id: testUUID(),
|
|
595
|
+
}, ctx);
|
|
596
|
+
|
|
597
|
+
expect(result.result).toMatchObject({
|
|
598
|
+
success: true,
|
|
599
|
+
message: 'Member removed. Their org-scoped API keys have been invalidated.',
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should throw error when delete fails', async () => {
|
|
604
|
+
const supabase = createMockSupabase({
|
|
605
|
+
deleteResult: { data: null, error: { message: 'Delete failed' } },
|
|
606
|
+
});
|
|
607
|
+
const ctx = createMockContext(supabase);
|
|
608
|
+
|
|
609
|
+
await expect(
|
|
610
|
+
removeMember({
|
|
611
|
+
organization_id: testUUID(),
|
|
612
|
+
user_id: testUUID(),
|
|
613
|
+
}, ctx)
|
|
614
|
+
).rejects.toThrow('Failed to remove member: Delete failed');
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// ============================================================================
|
|
619
|
+
// leaveOrganization Tests
|
|
620
|
+
// ============================================================================
|
|
621
|
+
|
|
622
|
+
describe('leaveOrganization', () => {
|
|
623
|
+
beforeEach(() => vi.clearAllMocks());
|
|
624
|
+
|
|
625
|
+
it('should throw error for missing organization_id', async () => {
|
|
626
|
+
const supabase = createMockSupabase();
|
|
627
|
+
const ctx = createMockContext(supabase);
|
|
628
|
+
|
|
629
|
+
await expect(leaveOrganization({}, ctx)).rejects.toThrow(ValidationError);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should throw error when user is owner', async () => {
|
|
633
|
+
const supabase = createMockSupabase({
|
|
634
|
+
selectResult: { data: { role: 'owner' }, error: null },
|
|
635
|
+
});
|
|
636
|
+
const ctx = createMockContext(supabase);
|
|
637
|
+
|
|
638
|
+
await expect(
|
|
639
|
+
leaveOrganization({ organization_id: testUUID() }, ctx)
|
|
640
|
+
).rejects.toThrow('Owner cannot leave. Transfer ownership first or delete the organization.');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it('should leave organization successfully', async () => {
|
|
644
|
+
const supabase = createMockSupabase({
|
|
645
|
+
selectResult: { data: { role: 'member' }, error: null },
|
|
646
|
+
deleteResult: { data: null, error: null },
|
|
647
|
+
});
|
|
648
|
+
const ctx = createMockContext(supabase);
|
|
649
|
+
|
|
650
|
+
const result = await leaveOrganization({ organization_id: testUUID() }, ctx);
|
|
651
|
+
|
|
652
|
+
expect(result.result).toMatchObject({
|
|
653
|
+
success: true,
|
|
654
|
+
message: 'You have left the organization.',
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('should throw error when delete fails', async () => {
|
|
659
|
+
const supabase = createMockSupabase({
|
|
660
|
+
selectResult: { data: { role: 'member' }, error: null },
|
|
661
|
+
deleteResult: { data: null, error: { message: 'Delete failed' } },
|
|
662
|
+
});
|
|
663
|
+
const ctx = createMockContext(supabase);
|
|
664
|
+
|
|
665
|
+
await expect(
|
|
666
|
+
leaveOrganization({ organization_id: testUUID() }, ctx)
|
|
667
|
+
).rejects.toThrow('Failed to leave organization: Delete failed');
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// ============================================================================
|
|
672
|
+
// shareProjectWithOrg Tests
|
|
673
|
+
// ============================================================================
|
|
674
|
+
|
|
675
|
+
describe('shareProjectWithOrg', () => {
|
|
676
|
+
beforeEach(() => vi.clearAllMocks());
|
|
677
|
+
|
|
678
|
+
it('should throw error for missing project_id', async () => {
|
|
679
|
+
const supabase = createMockSupabase();
|
|
680
|
+
const ctx = createMockContext(supabase);
|
|
681
|
+
|
|
682
|
+
await expect(
|
|
683
|
+
shareProjectWithOrg({ organization_id: testUUID() }, ctx)
|
|
684
|
+
).rejects.toThrow(ValidationError);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('should throw error for missing organization_id', async () => {
|
|
688
|
+
const supabase = createMockSupabase();
|
|
689
|
+
const ctx = createMockContext(supabase);
|
|
690
|
+
|
|
691
|
+
await expect(
|
|
692
|
+
shareProjectWithOrg({ project_id: testUUID() }, ctx)
|
|
693
|
+
).rejects.toThrow(ValidationError);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('should throw error for invalid permission', async () => {
|
|
697
|
+
const supabase = createMockSupabase();
|
|
698
|
+
const ctx = createMockContext(supabase);
|
|
699
|
+
|
|
700
|
+
await expect(
|
|
701
|
+
shareProjectWithOrg({
|
|
702
|
+
project_id: testUUID(),
|
|
703
|
+
organization_id: testUUID(),
|
|
704
|
+
permission: 'invalid',
|
|
705
|
+
}, ctx)
|
|
706
|
+
).rejects.toThrow('Invalid permission. Must be one of: read, write, admin');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('should throw error when project not found or not owned', async () => {
|
|
710
|
+
const supabase = createMockSupabase({
|
|
711
|
+
selectResult: { data: null, error: null },
|
|
712
|
+
});
|
|
713
|
+
const ctx = createMockContext(supabase);
|
|
714
|
+
|
|
715
|
+
await expect(
|
|
716
|
+
shareProjectWithOrg({
|
|
717
|
+
project_id: testUUID(),
|
|
718
|
+
organization_id: testUUID(),
|
|
719
|
+
}, ctx)
|
|
720
|
+
).rejects.toThrow('Project not found or you are not the owner');
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should share project successfully with default permission', async () => {
|
|
724
|
+
// Need to simulate multiple select calls
|
|
725
|
+
const supabase = createMockSupabase();
|
|
726
|
+
let selectCount = 0;
|
|
727
|
+
supabase.select = vi.fn(() => {
|
|
728
|
+
selectCount++;
|
|
729
|
+
return supabase;
|
|
730
|
+
});
|
|
731
|
+
supabase.single = vi.fn(() => {
|
|
732
|
+
if (selectCount === 1) {
|
|
733
|
+
// First call: get project
|
|
734
|
+
return Promise.resolve({ data: { id: testUUID(), name: 'Test Project' }, error: null });
|
|
735
|
+
}
|
|
736
|
+
// Insert result
|
|
737
|
+
return Promise.resolve({
|
|
738
|
+
data: { id: 'share-1', permission: 'read' },
|
|
739
|
+
error: null,
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
supabase.maybeSingle = vi.fn(() => {
|
|
743
|
+
// Check for existing share
|
|
744
|
+
return Promise.resolve({ data: null, error: null });
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const ctx = createMockContext(supabase);
|
|
748
|
+
|
|
749
|
+
const result = await shareProjectWithOrg({
|
|
750
|
+
project_id: testUUID(),
|
|
751
|
+
organization_id: testUUID(),
|
|
752
|
+
}, ctx);
|
|
753
|
+
|
|
754
|
+
expect(result.result).toMatchObject({
|
|
755
|
+
success: true,
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('should throw error when share already exists', async () => {
|
|
760
|
+
const supabase = createMockSupabase();
|
|
761
|
+
let selectCount = 0;
|
|
762
|
+
supabase.select = vi.fn(() => {
|
|
763
|
+
selectCount++;
|
|
764
|
+
return supabase;
|
|
765
|
+
});
|
|
766
|
+
supabase.single = vi.fn(() => {
|
|
767
|
+
// First call: get project
|
|
768
|
+
return Promise.resolve({ data: { id: testUUID(), name: 'Test Project' }, error: null });
|
|
769
|
+
});
|
|
770
|
+
supabase.maybeSingle = vi.fn(() => {
|
|
771
|
+
// Check for existing share - exists
|
|
772
|
+
return Promise.resolve({ data: { id: 'existing-share' }, error: null });
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
const ctx = createMockContext(supabase);
|
|
776
|
+
|
|
777
|
+
await expect(
|
|
778
|
+
shareProjectWithOrg({
|
|
779
|
+
project_id: testUUID(),
|
|
780
|
+
organization_id: testUUID(),
|
|
781
|
+
}, ctx)
|
|
782
|
+
).rejects.toThrow('Project is already shared with this organization');
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// ============================================================================
|
|
787
|
+
// updateProjectShare Tests
|
|
788
|
+
// ============================================================================
|
|
789
|
+
|
|
790
|
+
describe('updateProjectShare', () => {
|
|
791
|
+
beforeEach(() => vi.clearAllMocks());
|
|
792
|
+
|
|
793
|
+
it('should throw error for missing project_id', async () => {
|
|
794
|
+
const supabase = createMockSupabase();
|
|
795
|
+
const ctx = createMockContext(supabase);
|
|
796
|
+
|
|
797
|
+
await expect(
|
|
798
|
+
updateProjectShare({ organization_id: testUUID(), permission: 'write' }, ctx)
|
|
799
|
+
).rejects.toThrow(ValidationError);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('should throw error for missing organization_id', async () => {
|
|
803
|
+
const supabase = createMockSupabase();
|
|
804
|
+
const ctx = createMockContext(supabase);
|
|
805
|
+
|
|
806
|
+
await expect(
|
|
807
|
+
updateProjectShare({ project_id: testUUID(), permission: 'write' }, ctx)
|
|
808
|
+
).rejects.toThrow(ValidationError);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('should throw error for missing permission', async () => {
|
|
812
|
+
const supabase = createMockSupabase();
|
|
813
|
+
const ctx = createMockContext(supabase);
|
|
814
|
+
|
|
815
|
+
await expect(
|
|
816
|
+
updateProjectShare({ project_id: testUUID(), organization_id: testUUID() }, ctx)
|
|
817
|
+
).rejects.toThrow(ValidationError);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('should throw error for invalid permission', async () => {
|
|
821
|
+
const supabase = createMockSupabase();
|
|
822
|
+
const ctx = createMockContext(supabase);
|
|
823
|
+
|
|
824
|
+
await expect(
|
|
825
|
+
updateProjectShare({
|
|
826
|
+
project_id: testUUID(),
|
|
827
|
+
organization_id: testUUID(),
|
|
828
|
+
permission: 'invalid',
|
|
829
|
+
}, ctx)
|
|
830
|
+
).rejects.toThrow('Invalid permission. Must be one of: read, write, admin');
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('should update share permission successfully', async () => {
|
|
834
|
+
const supabase = createMockSupabase({
|
|
835
|
+
updateResult: {
|
|
836
|
+
data: { id: 'share-1', permission: 'write' },
|
|
837
|
+
error: null,
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
const ctx = createMockContext(supabase);
|
|
841
|
+
|
|
842
|
+
const result = await updateProjectShare({
|
|
843
|
+
project_id: testUUID(),
|
|
844
|
+
organization_id: testUUID(),
|
|
845
|
+
permission: 'write',
|
|
846
|
+
}, ctx);
|
|
847
|
+
|
|
848
|
+
expect(result.result).toMatchObject({
|
|
849
|
+
success: true,
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('should throw error when update fails', async () => {
|
|
854
|
+
const supabase = createMockSupabase({
|
|
855
|
+
updateResult: { data: null, error: { message: 'Update failed' } },
|
|
856
|
+
});
|
|
857
|
+
const ctx = createMockContext(supabase);
|
|
858
|
+
|
|
859
|
+
await expect(
|
|
860
|
+
updateProjectShare({
|
|
861
|
+
project_id: testUUID(),
|
|
862
|
+
organization_id: testUUID(),
|
|
863
|
+
permission: 'write',
|
|
864
|
+
}, ctx)
|
|
865
|
+
).rejects.toThrow('Failed to update share: Update failed');
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// ============================================================================
|
|
870
|
+
// unshareProject Tests
|
|
871
|
+
// ============================================================================
|
|
872
|
+
|
|
873
|
+
describe('unshareProject', () => {
|
|
874
|
+
beforeEach(() => vi.clearAllMocks());
|
|
875
|
+
|
|
876
|
+
it('should throw error for missing project_id', async () => {
|
|
877
|
+
const supabase = createMockSupabase();
|
|
878
|
+
const ctx = createMockContext(supabase);
|
|
879
|
+
|
|
880
|
+
await expect(
|
|
881
|
+
unshareProject({ organization_id: testUUID() }, ctx)
|
|
882
|
+
).rejects.toThrow(ValidationError);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('should throw error for missing organization_id', async () => {
|
|
886
|
+
const supabase = createMockSupabase();
|
|
887
|
+
const ctx = createMockContext(supabase);
|
|
888
|
+
|
|
889
|
+
await expect(
|
|
890
|
+
unshareProject({ project_id: testUUID() }, ctx)
|
|
891
|
+
).rejects.toThrow(ValidationError);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('should unshare project successfully', async () => {
|
|
895
|
+
const supabase = createMockSupabase({
|
|
896
|
+
deleteResult: { data: null, error: null },
|
|
897
|
+
});
|
|
898
|
+
const ctx = createMockContext(supabase);
|
|
899
|
+
|
|
900
|
+
const result = await unshareProject({
|
|
901
|
+
project_id: testUUID(),
|
|
902
|
+
organization_id: testUUID(),
|
|
903
|
+
}, ctx);
|
|
904
|
+
|
|
905
|
+
expect(result.result).toMatchObject({
|
|
906
|
+
success: true,
|
|
907
|
+
message: 'Project share removed. Org members can no longer access this project.',
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should throw error when delete fails', async () => {
|
|
912
|
+
const supabase = createMockSupabase({
|
|
913
|
+
deleteResult: { data: null, error: { message: 'Delete failed' } },
|
|
914
|
+
});
|
|
915
|
+
const ctx = createMockContext(supabase);
|
|
916
|
+
|
|
917
|
+
await expect(
|
|
918
|
+
unshareProject({
|
|
919
|
+
project_id: testUUID(),
|
|
920
|
+
organization_id: testUUID(),
|
|
921
|
+
}, ctx)
|
|
922
|
+
).rejects.toThrow('Failed to unshare project: Delete failed');
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// ============================================================================
|
|
927
|
+
// listProjectShares Tests
|
|
928
|
+
// ============================================================================
|
|
929
|
+
|
|
930
|
+
describe('listProjectShares', () => {
|
|
931
|
+
beforeEach(() => vi.clearAllMocks());
|
|
932
|
+
|
|
933
|
+
it('should throw error for missing project_id', async () => {
|
|
934
|
+
const supabase = createMockSupabase();
|
|
935
|
+
const ctx = createMockContext(supabase);
|
|
936
|
+
|
|
937
|
+
await expect(listProjectShares({}, ctx)).rejects.toThrow(ValidationError);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
941
|
+
const supabase = createMockSupabase();
|
|
942
|
+
const ctx = createMockContext(supabase);
|
|
943
|
+
|
|
944
|
+
await expect(
|
|
945
|
+
listProjectShares({ project_id: 'invalid' }, ctx)
|
|
946
|
+
).rejects.toThrow(ValidationError);
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('should return empty array when no shares', async () => {
|
|
950
|
+
const supabase = createMockSupabase({
|
|
951
|
+
selectResult: { data: [], error: null },
|
|
952
|
+
});
|
|
953
|
+
const ctx = createMockContext(supabase);
|
|
954
|
+
|
|
955
|
+
const result = await listProjectShares({ project_id: testUUID() }, ctx);
|
|
956
|
+
|
|
957
|
+
expect(result.result).toMatchObject({
|
|
958
|
+
shares: [],
|
|
959
|
+
count: 0,
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it('should return shares list', async () => {
|
|
964
|
+
const supabase = createMockSupabase({
|
|
965
|
+
selectResult: {
|
|
966
|
+
data: [
|
|
967
|
+
{
|
|
968
|
+
id: 'share-1',
|
|
969
|
+
permission: 'read',
|
|
970
|
+
shared_at: '2025-01-14T12:00:00Z',
|
|
971
|
+
shared_by: 'user-123',
|
|
972
|
+
organizations: { id: 'org-1', name: 'Test Org', slug: 'test-org' },
|
|
973
|
+
},
|
|
974
|
+
],
|
|
975
|
+
error: null,
|
|
976
|
+
},
|
|
977
|
+
});
|
|
978
|
+
const ctx = createMockContext(supabase);
|
|
979
|
+
|
|
980
|
+
const result = await listProjectShares({ project_id: testUUID() }, ctx);
|
|
981
|
+
|
|
982
|
+
expect(result.result).toMatchObject({
|
|
983
|
+
count: 1,
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('should throw error when query fails', async () => {
|
|
988
|
+
const supabase = createMockSupabase({
|
|
989
|
+
selectResult: { data: null, error: { message: 'Query failed' } },
|
|
990
|
+
});
|
|
991
|
+
const ctx = createMockContext(supabase);
|
|
992
|
+
|
|
993
|
+
await expect(
|
|
994
|
+
listProjectShares({ project_id: testUUID() }, ctx)
|
|
995
|
+
).rejects.toThrow('Failed to list shares: Query failed');
|
|
996
|
+
});
|
|
997
|
+
});
|