@vibe80/vibe80 0.1.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.
Files changed (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/bin/vibe80.js +176 -0
  4. package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
  5. package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
  6. package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
  7. package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
  8. package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
  9. package/client/dist/assets/browser-e3WgtMs-.js +8 -0
  10. package/client/dist/assets/index-CgqGyssr.css +32 -0
  11. package/client/dist/assets/index-DnwKjoj7.js +706 -0
  12. package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
  13. package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
  14. package/client/dist/favicon.ico +0 -0
  15. package/client/dist/favicon.png +0 -0
  16. package/client/dist/favicon.svg +35 -0
  17. package/client/dist/index.html +14 -0
  18. package/client/index.html +16 -0
  19. package/client/package.json +34 -0
  20. package/client/public/favicon.ico +0 -0
  21. package/client/public/favicon.png +0 -0
  22. package/client/public/favicon.svg +35 -0
  23. package/client/public/pwa-192x192.png +0 -0
  24. package/client/public/pwa-512x512.png +0 -0
  25. package/client/src/App.jsx +3131 -0
  26. package/client/src/assets/logo_small.png +0 -0
  27. package/client/src/assets/vibe80_dark.svg +51 -0
  28. package/client/src/assets/vibe80_light.svg +50 -0
  29. package/client/src/components/Chat/ChatComposer.jsx +228 -0
  30. package/client/src/components/Chat/ChatMessages.jsx +811 -0
  31. package/client/src/components/Chat/ChatToolbar.jsx +109 -0
  32. package/client/src/components/Chat/useChatComposer.js +462 -0
  33. package/client/src/components/Diff/DiffPanel.jsx +129 -0
  34. package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
  35. package/client/src/components/Logs/LogsPanel.jsx +80 -0
  36. package/client/src/components/SessionGate/SessionGate.jsx +874 -0
  37. package/client/src/components/Settings/SettingsPanel.jsx +212 -0
  38. package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
  39. package/client/src/components/Topbar/Topbar.jsx +101 -0
  40. package/client/src/components/WorktreeTabs.css +419 -0
  41. package/client/src/components/WorktreeTabs.jsx +604 -0
  42. package/client/src/hooks/useAttachments.jsx +125 -0
  43. package/client/src/hooks/useBacklog.js +254 -0
  44. package/client/src/hooks/useChatClear.js +90 -0
  45. package/client/src/hooks/useChatCollapse.js +42 -0
  46. package/client/src/hooks/useChatCommands.js +294 -0
  47. package/client/src/hooks/useChatExport.js +144 -0
  48. package/client/src/hooks/useChatMessagesState.js +69 -0
  49. package/client/src/hooks/useChatSend.js +158 -0
  50. package/client/src/hooks/useChatSocket.js +1239 -0
  51. package/client/src/hooks/useDiffNavigation.js +19 -0
  52. package/client/src/hooks/useExplorerActions.js +1184 -0
  53. package/client/src/hooks/useGitIdentity.js +114 -0
  54. package/client/src/hooks/useLayoutMode.js +31 -0
  55. package/client/src/hooks/useLocalPreferences.js +131 -0
  56. package/client/src/hooks/useMessageSync.js +30 -0
  57. package/client/src/hooks/useNotifications.js +132 -0
  58. package/client/src/hooks/usePaneNavigation.js +67 -0
  59. package/client/src/hooks/usePanelState.js +13 -0
  60. package/client/src/hooks/useProviderSelection.js +70 -0
  61. package/client/src/hooks/useRepoBranchesModels.js +218 -0
  62. package/client/src/hooks/useRepoStatus.js +350 -0
  63. package/client/src/hooks/useRpcLogActions.js +19 -0
  64. package/client/src/hooks/useRpcLogView.js +58 -0
  65. package/client/src/hooks/useSessionHandoff.js +97 -0
  66. package/client/src/hooks/useSessionLifecycle.js +287 -0
  67. package/client/src/hooks/useSessionReset.js +63 -0
  68. package/client/src/hooks/useSessionResync.js +77 -0
  69. package/client/src/hooks/useTerminalSession.js +328 -0
  70. package/client/src/hooks/useToolbarExport.js +27 -0
  71. package/client/src/hooks/useTurnInterrupt.js +43 -0
  72. package/client/src/hooks/useVibe80Forms.js +128 -0
  73. package/client/src/hooks/useWorkspaceAuth.js +932 -0
  74. package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
  75. package/client/src/hooks/useWorktrees.js +396 -0
  76. package/client/src/i18n.jsx +87 -0
  77. package/client/src/index.css +5147 -0
  78. package/client/src/locales/en.json +37 -0
  79. package/client/src/locales/fr.json +321 -0
  80. package/client/src/main.jsx +16 -0
  81. package/client/vite.config.js +62 -0
  82. package/docs/api/asyncapi.json +1511 -0
  83. package/docs/api/openapi.json +3242 -0
  84. package/git_hooks/prepare-commit-msg +35 -0
  85. package/package.json +36 -0
  86. package/server/package.json +29 -0
  87. package/server/scripts/rotate-workspace-secret.js +101 -0
  88. package/server/src/claudeClient.js +454 -0
  89. package/server/src/clientEvents.js +594 -0
  90. package/server/src/clientFactory.js +164 -0
  91. package/server/src/codexClient.js +468 -0
  92. package/server/src/config.js +27 -0
  93. package/server/src/helpers.js +138 -0
  94. package/server/src/index.js +1641 -0
  95. package/server/src/middleware/auth.js +93 -0
  96. package/server/src/middleware/debug.js +89 -0
  97. package/server/src/middleware/errorTypes.js +60 -0
  98. package/server/src/providerLogger.js +60 -0
  99. package/server/src/routes/files.js +114 -0
  100. package/server/src/routes/git.js +183 -0
  101. package/server/src/routes/health.js +13 -0
  102. package/server/src/routes/sessions.js +407 -0
  103. package/server/src/routes/workspaces.js +296 -0
  104. package/server/src/routes/worktrees.js +993 -0
  105. package/server/src/runAs.js +458 -0
  106. package/server/src/runtimeStore.js +32 -0
  107. package/server/src/services/auth.js +157 -0
  108. package/server/src/services/claudeThreadDirectory.js +33 -0
  109. package/server/src/services/session.js +918 -0
  110. package/server/src/services/workspace.js +858 -0
  111. package/server/src/storage/index.js +17 -0
  112. package/server/src/storage/redis.js +412 -0
  113. package/server/src/storage/sqlite.js +649 -0
  114. package/server/src/worktreeManager.js +717 -0
  115. package/server/tests/README.md +13 -0
  116. package/server/tests/factories/workspaceFactory.js +13 -0
  117. package/server/tests/fixtures/workspaceCredentials.json +4 -0
  118. package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
  119. package/server/tests/setup/env.js +9 -0
  120. package/server/tests/unit/helpers.test.js +95 -0
  121. package/server/tests/unit/services/auth.test.js +181 -0
  122. package/server/tests/unit/services/workspace.test.js +115 -0
  123. package/server/vitest.config.js +23 -0
@@ -0,0 +1,13 @@
1
+ # Tests serveur
2
+
3
+ ## Commandes
4
+
5
+ - `npm --workspace server run test`
6
+ - `npm --workspace server run test:watch`
7
+ - `npm --workspace server run test:coverage`
8
+
9
+ ## Structure
10
+
11
+ - `tests/setup/` : bootstrap d'environnement
12
+ - `tests/unit/` : tests unitaires
13
+ - `tests/integration/` : tests d'intégration des routes
@@ -0,0 +1,13 @@
1
+ export const makeWorkspaceCredentials = (overrides = {}) => ({
2
+ workspaceId: "w0123456789abcdef01234567",
3
+ workspaceSecret: "test-workspace-secret",
4
+ ...overrides,
5
+ });
6
+
7
+ export const makeWorkspaceTokens = (overrides = {}) => ({
8
+ workspaceToken: "workspace-token",
9
+ refreshToken: "refresh-token",
10
+ expiresIn: 3600,
11
+ refreshExpiresIn: 2592000,
12
+ ...overrides,
13
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "workspaceId": "w0123456789abcdef01234567",
3
+ "workspaceSecret": "test-workspace-secret"
4
+ }
@@ -0,0 +1,626 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { makeWorkspaceCredentials } from "../../factories/workspaceFactory.js";
3
+
4
+ const verifyWorkspaceSecretMock = vi.hoisted(() => vi.fn());
5
+ const issueWorkspaceTokensMock = vi.hoisted(() => vi.fn());
6
+ const rotateWorkspaceRefreshTokenMock = vi.hoisted(() => vi.fn());
7
+ const consumeMonoAuthTokenMock = vi.hoisted(() => vi.fn());
8
+ const createWorkspaceMock = vi.hoisted(() => vi.fn());
9
+ const appendAuditLogMock = vi.hoisted(() => vi.fn());
10
+ const getWorkspaceUserIdsMock = vi.hoisted(() => vi.fn());
11
+ const readWorkspaceConfigMock = vi.hoisted(() => vi.fn());
12
+ const sanitizeProvidersForResponseMock = vi.hoisted(() => vi.fn((providers) => providers || {}));
13
+ const updateWorkspaceMock = vi.hoisted(() => vi.fn());
14
+ const mergeProvidersForUpdateMock = vi.hoisted(() =>
15
+ vi.fn((existing, incoming) => ({ ...existing, ...incoming }))
16
+ );
17
+ const storageMock = vi.hoisted(() => ({
18
+ listSessions: vi.fn(async () => []),
19
+ }));
20
+
21
+ vi.mock("../../../src/storage/index.js", () => ({
22
+ default: storageMock,
23
+ }));
24
+
25
+ vi.mock("../../../src/services/auth.js", () => ({
26
+ consumeMonoAuthToken: consumeMonoAuthTokenMock,
27
+ issueWorkspaceTokens: issueWorkspaceTokensMock,
28
+ rotateWorkspaceRefreshToken: rotateWorkspaceRefreshTokenMock,
29
+ }));
30
+
31
+ vi.mock("../../../src/runtimeStore.js", () => ({
32
+ getExistingSessionRuntime: vi.fn(() => null),
33
+ }));
34
+
35
+ vi.mock("../../../src/services/workspace.js", () => ({
36
+ workspaceIdPattern: /^w[0-9a-f]{24}$/,
37
+ createWorkspace: createWorkspaceMock,
38
+ updateWorkspace: updateWorkspaceMock,
39
+ readWorkspaceConfig: readWorkspaceConfigMock,
40
+ verifyWorkspaceSecret: verifyWorkspaceSecretMock,
41
+ getWorkspaceUserIds: getWorkspaceUserIdsMock,
42
+ sanitizeProvidersForResponse: sanitizeProvidersForResponseMock,
43
+ mergeProvidersForUpdate: mergeProvidersForUpdateMock,
44
+ appendAuditLog: appendAuditLogMock,
45
+ }));
46
+
47
+ describe("routes/workspaces", () => {
48
+ const validCredentials = makeWorkspaceCredentials();
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ process.env.DEPLOYMENT_MODE = "multi_user";
53
+ issueWorkspaceTokensMock.mockResolvedValue({
54
+ workspaceToken: "workspace-token",
55
+ refreshToken: "refresh-token",
56
+ expiresIn: 3600,
57
+ refreshExpiresIn: 86400,
58
+ });
59
+ consumeMonoAuthTokenMock.mockReturnValue({
60
+ ok: false,
61
+ code: "MONO_AUTH_TOKEN_INVALID",
62
+ });
63
+ getWorkspaceUserIdsMock.mockResolvedValue({ uid: 200001, gid: 200001 });
64
+ appendAuditLogMock.mockResolvedValue();
65
+ storageMock.listSessions.mockResolvedValue([]);
66
+ rotateWorkspaceRefreshTokenMock.mockResolvedValue({
67
+ ok: true,
68
+ payload: {
69
+ workspaceToken: "workspace-token",
70
+ refreshToken: "refresh-token",
71
+ expiresIn: 3600,
72
+ refreshExpiresIn: 86400,
73
+ },
74
+ });
75
+ createWorkspaceMock.mockResolvedValue({
76
+ workspaceId: validCredentials.workspaceId,
77
+ workspaceSecret: validCredentials.workspaceSecret,
78
+ });
79
+ readWorkspaceConfigMock.mockResolvedValue({
80
+ providers: {
81
+ codex: { enabled: true },
82
+ },
83
+ });
84
+ updateWorkspaceMock.mockResolvedValue({
85
+ providers: {
86
+ codex: { enabled: true },
87
+ },
88
+ });
89
+ });
90
+
91
+ it("POST /api/v1/workspaces/login mono_auth_token renvoie 403 hors mono_user", async () => {
92
+ process.env.DEPLOYMENT_MODE = "multi_user";
93
+ const handler = await createRouteHandler("/workspaces/login", "post");
94
+ const res = createMockRes();
95
+ await handler(
96
+ { body: { grantType: "mono_auth_token", monoAuthToken: "token-1" } },
97
+ res
98
+ );
99
+
100
+ expect(res.statusCode).toBe(403);
101
+ expect(res.body).toMatchObject({
102
+ error_type: "MONO_AUTH_FORBIDDEN",
103
+ });
104
+ });
105
+
106
+ it("POST /api/v1/workspaces renvoie 403 en mode mono_user", async () => {
107
+ process.env.DEPLOYMENT_MODE = "mono_user";
108
+ const handler = await createRouteHandler("/workspaces", "post");
109
+ const res = createMockRes();
110
+ await handler(
111
+ {
112
+ body: {
113
+ providers: { codex: { enabled: true, auth: { type: "api_key", value: "x" } } },
114
+ },
115
+ },
116
+ res
117
+ );
118
+
119
+ expect(res.statusCode).toBe(403);
120
+ expect(res.body).toMatchObject({
121
+ error_type: "WORKSPACE_CREATE_FORBIDDEN",
122
+ });
123
+ expect(createWorkspaceMock).not.toHaveBeenCalled();
124
+ });
125
+
126
+ it("POST /api/v1/workspaces/login credentials renvoie 403 en mode mono_user", async () => {
127
+ process.env.DEPLOYMENT_MODE = "mono_user";
128
+ const handler = await createRouteHandler("/workspaces/login", "post");
129
+ const res = createMockRes();
130
+ await handler(
131
+ {
132
+ body: {
133
+ workspaceId: validCredentials.workspaceId,
134
+ workspaceSecret: "any-secret",
135
+ },
136
+ },
137
+ res
138
+ );
139
+
140
+ expect(res.statusCode).toBe(403);
141
+ expect(res.body).toMatchObject({
142
+ error_type: "WORKSPACE_LOGIN_FORBIDDEN",
143
+ });
144
+ expect(verifyWorkspaceSecretMock).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it("POST /api/v1/workspaces/login mono_auth_token renvoie 400 si token manquant", async () => {
148
+ process.env.DEPLOYMENT_MODE = "mono_user";
149
+ const handler = await createRouteHandler("/workspaces/login", "post");
150
+ const res = createMockRes();
151
+ await handler({ body: { grantType: "mono_auth_token" } }, res);
152
+
153
+ expect(res.statusCode).toBe(400);
154
+ expect(res.body).toMatchObject({
155
+ error_type: "MONO_AUTH_TOKEN_REQUIRED",
156
+ });
157
+ });
158
+
159
+ it("POST /api/v1/workspaces/login mono_auth_token renvoie 401 si token invalide", async () => {
160
+ process.env.DEPLOYMENT_MODE = "mono_user";
161
+ consumeMonoAuthTokenMock.mockReturnValueOnce({
162
+ ok: false,
163
+ code: "MONO_AUTH_TOKEN_INVALID",
164
+ });
165
+ const handler = await createRouteHandler("/workspaces/login", "post");
166
+ const res = createMockRes();
167
+ await handler(
168
+ { body: { grantType: "mono_auth_token", monoAuthToken: "bad-token" } },
169
+ res
170
+ );
171
+
172
+ expect(res.statusCode).toBe(401);
173
+ expect(res.body).toMatchObject({
174
+ error_type: "MONO_AUTH_TOKEN_INVALID",
175
+ });
176
+ });
177
+
178
+ it("POST /api/v1/workspaces/login mono_auth_token renvoie 401 si workspace non résolu", async () => {
179
+ process.env.DEPLOYMENT_MODE = "mono_user";
180
+ consumeMonoAuthTokenMock.mockReturnValueOnce({
181
+ ok: true,
182
+ workspaceId: validCredentials.workspaceId,
183
+ });
184
+ getWorkspaceUserIdsMock.mockRejectedValueOnce(new Error("Workspace not found."));
185
+ const handler = await createRouteHandler("/workspaces/login", "post");
186
+ const res = createMockRes();
187
+ await handler(
188
+ { body: { grantType: "mono_auth_token", monoAuthToken: "token-x" } },
189
+ res
190
+ );
191
+
192
+ expect(res.statusCode).toBe(401);
193
+ expect(res.body).toMatchObject({
194
+ error_type: "MONO_AUTH_TOKEN_INVALID",
195
+ });
196
+ expect(appendAuditLogMock).toHaveBeenCalledWith(
197
+ validCredentials.workspaceId,
198
+ "workspace_login_failed",
199
+ { grantType: "mono_auth_token" }
200
+ );
201
+ });
202
+
203
+ it("POST /api/v1/workspaces/login mono_auth_token renvoie 200 en succès", async () => {
204
+ process.env.DEPLOYMENT_MODE = "mono_user";
205
+ consumeMonoAuthTokenMock.mockReturnValueOnce({
206
+ ok: true,
207
+ workspaceId: validCredentials.workspaceId,
208
+ });
209
+ const handler = await createRouteHandler("/workspaces/login", "post");
210
+ const res = createMockRes();
211
+ await handler(
212
+ { body: { grantType: "mono_auth_token", monoAuthToken: "token-ok" } },
213
+ res
214
+ );
215
+
216
+ expect(res.statusCode).toBe(200);
217
+ expect(res.body).toMatchObject({
218
+ workspaceToken: "workspace-token",
219
+ refreshToken: "refresh-token",
220
+ });
221
+ expect(issueWorkspaceTokensMock).toHaveBeenCalledWith(validCredentials.workspaceId);
222
+ expect(appendAuditLogMock).toHaveBeenCalledWith(
223
+ validCredentials.workspaceId,
224
+ "workspace_login_success",
225
+ { grantType: "mono_auth_token" }
226
+ );
227
+ });
228
+
229
+ const createRouteHandler = async (path, method) => {
230
+ const { default: workspaceRoutes } = await import("../../../src/routes/workspaces.js");
231
+ const router = workspaceRoutes();
232
+ const layer = router.stack.find(
233
+ (entry) =>
234
+ entry.route &&
235
+ entry.route.path === path &&
236
+ Boolean(entry.route.methods?.[String(method || "").toLowerCase()])
237
+ );
238
+ if (!layer) {
239
+ throw new Error(`Route not found: ${method.toUpperCase()} ${path}`);
240
+ }
241
+ return layer.route.stack[0].handle;
242
+ };
243
+
244
+ const createMockRes = () => {
245
+ const res = {
246
+ statusCode: 200,
247
+ body: null,
248
+ status(code) {
249
+ this.statusCode = code;
250
+ return this;
251
+ },
252
+ json(payload) {
253
+ this.body = payload;
254
+ return this;
255
+ },
256
+ };
257
+ return res;
258
+ };
259
+
260
+ it("POST /api/v1/workspaces/login renvoie 401 si credentials manquants", async () => {
261
+ const handler = await createRouteHandler("/workspaces/login", "post");
262
+ const res = createMockRes();
263
+ await handler({ body: {} }, res);
264
+ expect(res.statusCode).toBe(401);
265
+ expect(res.body.error).toBe("Invalid workspace credentials.");
266
+ });
267
+
268
+ it("POST /api/v1/workspaces/login renvoie 401 si secret invalide", async () => {
269
+ verifyWorkspaceSecretMock.mockResolvedValueOnce(false);
270
+ const handler = await createRouteHandler("/workspaces/login", "post");
271
+ const res = createMockRes();
272
+ await handler(
273
+ {
274
+ body: {
275
+ workspaceId: validCredentials.workspaceId,
276
+ workspaceSecret: "bad-secret",
277
+ },
278
+ },
279
+ res
280
+ );
281
+
282
+ expect(res.statusCode).toBe(401);
283
+ expect(res.body.error).toBe("Invalid workspace credentials.");
284
+ expect(appendAuditLogMock).toHaveBeenCalledWith(
285
+ validCredentials.workspaceId,
286
+ "workspace_login_failed"
287
+ );
288
+ });
289
+
290
+ it("POST /api/v1/workspaces/login renvoie 200 si credentials valides", async () => {
291
+ verifyWorkspaceSecretMock.mockResolvedValueOnce(true);
292
+ const handler = await createRouteHandler("/workspaces/login", "post");
293
+ const res = createMockRes();
294
+ await handler(
295
+ {
296
+ body: {
297
+ workspaceId: validCredentials.workspaceId,
298
+ workspaceSecret: "good-secret",
299
+ },
300
+ },
301
+ res
302
+ );
303
+
304
+ expect(res.statusCode).toBe(200);
305
+ expect(res.body).toMatchObject({
306
+ workspaceToken: "workspace-token",
307
+ refreshToken: "refresh-token",
308
+ });
309
+ expect(getWorkspaceUserIdsMock).toHaveBeenCalledWith(validCredentials.workspaceId);
310
+ expect(issueWorkspaceTokensMock).toHaveBeenCalledWith(validCredentials.workspaceId);
311
+ expect(appendAuditLogMock).toHaveBeenCalledWith(
312
+ validCredentials.workspaceId,
313
+ "workspace_login_success"
314
+ );
315
+ });
316
+
317
+ it("POST /api/v1/workspaces/refresh renvoie 400 si refreshToken manquant", async () => {
318
+ const handler = await createRouteHandler("/workspaces/refresh", "post");
319
+ const res = createMockRes();
320
+ await handler({ body: {} }, res);
321
+
322
+ expect(res.statusCode).toBe(400);
323
+ expect(res.body).toEqual({ error: "refreshToken is required." });
324
+ });
325
+
326
+ it("POST /api/v1/workspaces/refresh renvoie 401 si refresh token invalide", async () => {
327
+ rotateWorkspaceRefreshTokenMock.mockResolvedValueOnce({
328
+ ok: false,
329
+ status: 401,
330
+ payload: {
331
+ error: "Invalid refresh token.",
332
+ code: "invalid_refresh_token",
333
+ },
334
+ });
335
+ const handler = await createRouteHandler("/workspaces/refresh", "post");
336
+ const res = createMockRes();
337
+ await handler({ body: { refreshToken: "bad-refresh-token" } }, res);
338
+
339
+ expect(res.statusCode).toBe(401);
340
+ expect(res.body).toEqual({
341
+ error: "Invalid refresh token.",
342
+ code: "invalid_refresh_token",
343
+ });
344
+ });
345
+
346
+ it("POST /api/v1/workspaces/refresh renvoie 401 si refresh token expiré", async () => {
347
+ rotateWorkspaceRefreshTokenMock.mockResolvedValueOnce({
348
+ ok: false,
349
+ status: 401,
350
+ payload: {
351
+ error: "Refresh token expired.",
352
+ code: "refresh_token_expired",
353
+ },
354
+ });
355
+ const handler = await createRouteHandler("/workspaces/refresh", "post");
356
+ const res = createMockRes();
357
+ await handler({ body: { refreshToken: "expired-refresh-token" } }, res);
358
+
359
+ expect(res.statusCode).toBe(401);
360
+ expect(res.body).toEqual({
361
+ error: "Refresh token expired.",
362
+ code: "refresh_token_expired",
363
+ });
364
+ expect(rotateWorkspaceRefreshTokenMock).toHaveBeenCalledWith("expired-refresh-token");
365
+ });
366
+
367
+ it("POST /api/v1/workspaces/refresh renvoie 401 si refresh token réutilisé", async () => {
368
+ rotateWorkspaceRefreshTokenMock.mockResolvedValueOnce({
369
+ ok: false,
370
+ status: 401,
371
+ payload: {
372
+ error: "Refresh token reused.",
373
+ code: "refresh_token_reused",
374
+ },
375
+ });
376
+ const handler = await createRouteHandler("/workspaces/refresh", "post");
377
+ const res = createMockRes();
378
+ await handler({ body: { refreshToken: "reused-refresh-token" } }, res);
379
+
380
+ expect(res.statusCode).toBe(401);
381
+ expect(res.body).toEqual({
382
+ error: "Refresh token reused.",
383
+ code: "refresh_token_reused",
384
+ });
385
+ expect(rotateWorkspaceRefreshTokenMock).toHaveBeenCalledWith("reused-refresh-token");
386
+ });
387
+
388
+ it("POST /api/v1/workspaces/refresh renvoie 200 si refresh token valide", async () => {
389
+ rotateWorkspaceRefreshTokenMock.mockResolvedValueOnce({
390
+ ok: true,
391
+ payload: {
392
+ workspaceToken: "workspace-token",
393
+ refreshToken: "refresh-token",
394
+ expiresIn: 3600,
395
+ refreshExpiresIn: 86400,
396
+ },
397
+ });
398
+ const handler = await createRouteHandler("/workspaces/refresh", "post");
399
+ const res = createMockRes();
400
+ await handler({ body: { refreshToken: "valid-refresh-token" } }, res);
401
+
402
+ expect(res.statusCode).toBe(200);
403
+ expect(res.body).toMatchObject({
404
+ workspaceToken: "workspace-token",
405
+ refreshToken: "refresh-token",
406
+ });
407
+ expect(rotateWorkspaceRefreshTokenMock).toHaveBeenCalledWith("valid-refresh-token");
408
+ });
409
+
410
+ it("POST /api/v1/workspaces/refresh renvoie 500 en cas d’erreur interne", async () => {
411
+ rotateWorkspaceRefreshTokenMock.mockRejectedValueOnce(new Error("storage down"));
412
+ const handler = await createRouteHandler("/workspaces/refresh", "post");
413
+ const res = createMockRes();
414
+ await handler({ body: { refreshToken: "valid-refresh-token" } }, res);
415
+
416
+ expect(res.statusCode).toBe(500);
417
+ expect(res.body).toEqual({ error: "Failed to refresh workspace token." });
418
+ });
419
+
420
+ it("GET /api/v1/workspaces/:workspaceId renvoie 400 si workspaceId invalide", async () => {
421
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "get");
422
+ const res = createMockRes();
423
+ await handler({ params: { workspaceId: "invalid-id" } }, res);
424
+
425
+ expect(res.statusCode).toBe(400);
426
+ expect(res.body).toEqual({ error: "Invalid workspaceId." });
427
+ });
428
+
429
+ it("GET /api/v1/workspaces/:workspaceId renvoie 403 si workspaceId ne correspond pas au token", async () => {
430
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "get");
431
+ const res = createMockRes();
432
+ await handler(
433
+ {
434
+ params: { workspaceId: validCredentials.workspaceId },
435
+ workspaceId: "wffffffffffffffffffffffff",
436
+ },
437
+ res
438
+ );
439
+
440
+ expect(res.statusCode).toBe(403);
441
+ expect(res.body).toEqual({ error: "Forbidden." });
442
+ });
443
+
444
+ it("GET /api/v1/workspaces/:workspaceId renvoie 200 si accès autorisé", async () => {
445
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "get");
446
+ const res = createMockRes();
447
+ await handler(
448
+ {
449
+ params: { workspaceId: validCredentials.workspaceId },
450
+ workspaceId: validCredentials.workspaceId,
451
+ },
452
+ res
453
+ );
454
+
455
+ expect(res.statusCode).toBe(200);
456
+ expect(res.body).toEqual({
457
+ workspaceId: validCredentials.workspaceId,
458
+ providers: {
459
+ codex: { enabled: true },
460
+ },
461
+ });
462
+ expect(readWorkspaceConfigMock).toHaveBeenCalledWith(validCredentials.workspaceId);
463
+ expect(sanitizeProvidersForResponseMock).toHaveBeenCalled();
464
+ });
465
+
466
+ it("GET /api/v1/workspaces/:workspaceId renvoie 400 si la lecture échoue", async () => {
467
+ readWorkspaceConfigMock.mockRejectedValueOnce(new Error("Workspace not found."));
468
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "get");
469
+ const res = createMockRes();
470
+ await handler(
471
+ {
472
+ params: { workspaceId: validCredentials.workspaceId },
473
+ workspaceId: validCredentials.workspaceId,
474
+ },
475
+ res
476
+ );
477
+
478
+ expect(res.statusCode).toBe(400);
479
+ expect(res.body).toEqual({ error: "Workspace not found." });
480
+ });
481
+
482
+ it("PATCH /api/v1/workspaces/:workspaceId renvoie 400 si workspaceId invalide", async () => {
483
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "patch");
484
+ const res = createMockRes();
485
+ await handler({ params: { workspaceId: "invalid-id" }, body: {} }, res);
486
+
487
+ expect(res.statusCode).toBe(400);
488
+ expect(res.body).toEqual({ error: "Invalid workspaceId." });
489
+ });
490
+
491
+ it("PATCH /api/v1/workspaces/:workspaceId renvoie 403 si accès interdit", async () => {
492
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "patch");
493
+ const res = createMockRes();
494
+ await handler(
495
+ {
496
+ params: { workspaceId: validCredentials.workspaceId },
497
+ workspaceId: "wffffffffffffffffffffffff",
498
+ body: {},
499
+ },
500
+ res
501
+ );
502
+
503
+ expect(res.statusCode).toBe(403);
504
+ expect(res.body).toEqual({ error: "Forbidden." });
505
+ });
506
+
507
+ it("PATCH /api/v1/workspaces/:workspaceId renvoie 403 si provider actif en session", async () => {
508
+ mergeProvidersForUpdateMock.mockReturnValueOnce({
509
+ codex: { enabled: false },
510
+ });
511
+ readWorkspaceConfigMock.mockResolvedValueOnce({
512
+ providers: {
513
+ codex: { enabled: true },
514
+ },
515
+ });
516
+ storageMock.listSessions.mockResolvedValueOnce([
517
+ { sessionId: "s1", activeProvider: "codex" },
518
+ ]);
519
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "patch");
520
+ const res = createMockRes();
521
+ await handler(
522
+ {
523
+ params: { workspaceId: validCredentials.workspaceId },
524
+ workspaceId: validCredentials.workspaceId,
525
+ body: { providers: { codex: { enabled: false } } },
526
+ },
527
+ res
528
+ );
529
+
530
+ expect(res.statusCode).toBe(403);
531
+ expect(res.body).toEqual({
532
+ error: "Provider cannot be disabled: active sessions use it.",
533
+ });
534
+ expect(updateWorkspaceMock).not.toHaveBeenCalled();
535
+ });
536
+
537
+ it("PATCH /api/v1/workspaces/:workspaceId renvoie 200 en succès", async () => {
538
+ mergeProvidersForUpdateMock.mockReturnValueOnce({
539
+ codex: { enabled: true },
540
+ });
541
+ updateWorkspaceMock.mockResolvedValueOnce({
542
+ providers: {
543
+ codex: { enabled: true },
544
+ },
545
+ });
546
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "patch");
547
+ const res = createMockRes();
548
+ await handler(
549
+ {
550
+ params: { workspaceId: validCredentials.workspaceId },
551
+ workspaceId: validCredentials.workspaceId,
552
+ body: { providers: { codex: { enabled: true } } },
553
+ },
554
+ res
555
+ );
556
+
557
+ expect(res.statusCode).toBe(200);
558
+ expect(res.body).toEqual({
559
+ workspaceId: validCredentials.workspaceId,
560
+ providers: {
561
+ codex: { enabled: true },
562
+ },
563
+ });
564
+ expect(updateWorkspaceMock).toHaveBeenCalledWith(validCredentials.workspaceId, {
565
+ codex: { enabled: true },
566
+ });
567
+ });
568
+
569
+ it("PATCH /api/v1/workspaces/:workspaceId renvoie 400 si update échoue", async () => {
570
+ updateWorkspaceMock.mockRejectedValueOnce(new Error("Failed to update workspace."));
571
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "patch");
572
+ const res = createMockRes();
573
+ await handler(
574
+ {
575
+ params: { workspaceId: validCredentials.workspaceId },
576
+ workspaceId: validCredentials.workspaceId,
577
+ body: { providers: { codex: { enabled: true } } },
578
+ },
579
+ res
580
+ );
581
+
582
+ expect(res.statusCode).toBe(400);
583
+ expect(res.body).toEqual({ error: "Failed to update workspace." });
584
+ });
585
+
586
+ it("DELETE /api/v1/workspaces/:workspaceId renvoie 400 si workspaceId invalide", async () => {
587
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "delete");
588
+ const res = createMockRes();
589
+ await handler({ params: { workspaceId: "invalid-id" } }, res);
590
+
591
+ expect(res.statusCode).toBe(400);
592
+ expect(res.body).toEqual({ error: "Invalid workspaceId." });
593
+ });
594
+
595
+ it("DELETE /api/v1/workspaces/:workspaceId renvoie 403 si accès interdit", async () => {
596
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "delete");
597
+ const res = createMockRes();
598
+ await handler(
599
+ {
600
+ params: { workspaceId: validCredentials.workspaceId },
601
+ workspaceId: "wffffffffffffffffffffffff",
602
+ },
603
+ res
604
+ );
605
+
606
+ expect(res.statusCode).toBe(403);
607
+ expect(res.body).toEqual({ error: "Forbidden." });
608
+ });
609
+
610
+ it("DELETE /api/v1/workspaces/:workspaceId renvoie 405 (désactivé)", async () => {
611
+ const handler = await createRouteHandler("/workspaces/:workspaceId", "delete");
612
+ const res = createMockRes();
613
+ await handler(
614
+ {
615
+ params: { workspaceId: validCredentials.workspaceId },
616
+ workspaceId: validCredentials.workspaceId,
617
+ },
618
+ res
619
+ );
620
+
621
+ expect(res.statusCode).toBe(405);
622
+ expect(res.body).toEqual({
623
+ error: "Workspace deletion is currently disabled.",
624
+ });
625
+ });
626
+ });
@@ -0,0 +1,9 @@
1
+ import path from "path";
2
+ import os from "os";
3
+
4
+ process.env.NODE_ENV = "test";
5
+ process.env.STORAGE_BACKEND = process.env.STORAGE_BACKEND || "sqlite";
6
+ process.env.DEPLOYMENT_MODE = process.env.DEPLOYMENT_MODE || "multi_user";
7
+ process.env.JWT_KEY = process.env.JWT_KEY || "test-jwt-key";
8
+ process.env.JWT_KEY_PATH =
9
+ process.env.JWT_KEY_PATH || path.join(os.tmpdir(), "vibe80-jwt-test.key");