@jskit-ai/users-core 0.1.4
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/package.descriptor.mjs +464 -0
- package/package.json +35 -0
- package/src/server/UsersCoreServiceProvider.js +74 -0
- package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
- package/src/server/accountNotifications/accountNotificationsService.js +41 -0
- package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
- package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
- package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
- package/src/server/accountPreferences/accountPreferencesService.js +41 -0
- package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
- package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
- package/src/server/accountProfile/accountProfileActions.js +137 -0
- package/src/server/accountProfile/accountProfileService.js +124 -0
- package/src/server/accountProfile/avatarService.js +141 -0
- package/src/server/accountProfile/avatarStorageService.js +132 -0
- package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
- package/src/server/accountProfile/registerAccountProfile.js +62 -0
- package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
- package/src/server/accountSecurity/accountSecurityActions.js +144 -0
- package/src/server/accountSecurity/accountSecurityService.js +103 -0
- package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
- package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
- package/src/server/common/README.md +21 -0
- package/src/server/common/contributors/README.md +11 -0
- package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
- package/src/server/common/diTokens.js +21 -0
- package/src/server/common/formatters/README.md +11 -0
- package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
- package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
- package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
- package/src/server/common/formatters/workspaceFormatter.js +46 -0
- package/src/server/common/registerCommonRepositories.js +45 -0
- package/src/server/common/registerSharedApi.js +9 -0
- package/src/server/common/repositories/README.md +24 -0
- package/src/server/common/repositories/repositoryUtils.js +50 -0
- package/src/server/common/repositories/userProfilesRepository.js +251 -0
- package/src/server/common/repositories/userSettingsRepository.js +179 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
- package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
- package/src/server/common/repositories/workspacesRepository.js +183 -0
- package/src/server/common/routes/README.md +11 -0
- package/src/server/common/services/README.md +12 -0
- package/src/server/common/services/accountContextService.js +31 -0
- package/src/server/common/services/authProfileSyncService.js +128 -0
- package/src/server/common/services/workspaceContextService.js +270 -0
- package/src/server/common/support/deepFreeze.js +17 -0
- package/src/server/common/support/realtimeServiceEvents.js +94 -0
- package/src/server/common/support/resolveActionUser.js +11 -0
- package/src/server/common/support/workspaceRoutePaths.js +17 -0
- package/src/server/common/validators/README.md +11 -0
- package/src/server/common/validators/authenticatedUserValidator.js +42 -0
- package/src/server/common/validators/routeParamsValidator.js +62 -0
- package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
- package/src/server/consoleSettings/consoleService.js +36 -0
- package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
- package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
- package/src/server/consoleSettings/consoleSettingsService.js +40 -0
- package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
- package/src/server/registerWorkspaceBootstrap.js +36 -0
- package/src/server/registerWorkspaceCore.js +95 -0
- package/src/server/support/resolveWorkspace.js +16 -0
- package/src/server/support/workspaceActionSurfaces.js +135 -0
- package/src/server/support/workspaceInvitationsPolicy.js +45 -0
- package/src/server/support/workspaceRouteInput.js +22 -0
- package/src/server/workspaceBootstrapContributor.js +401 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
- package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
- package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
- package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
- package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
- package/src/shared/events/usersEvents.js +19 -0
- package/src/shared/index.js +91 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/consoleSettingsFields.js +55 -0
- package/src/shared/resources/consoleSettingsResource.js +139 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/src/shared/resources/userProfileResource.js +148 -0
- package/src/shared/resources/userSettingsFields.js +71 -0
- package/src/shared/resources/userSettingsResource.js +416 -0
- package/src/shared/resources/workspaceMembersResource.js +352 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
- package/src/shared/resources/workspaceResource.js +149 -0
- package/src/shared/resources/workspaceSettingsFields.js +60 -0
- package/src/shared/resources/workspaceSettingsResource.js +178 -0
- package/src/shared/roles.js +136 -0
- package/src/shared/settings.js +31 -0
- package/src/shared/support/usersApiPaths.js +34 -0
- package/src/shared/support/usersVisibility.js +45 -0
- package/src/shared/support/workspacePathModel.js +145 -0
- package/src/shared/tenancyMode.js +35 -0
- package/src/shared/tenancyProfile.js +73 -0
- package/templates/config/workspaceRoles.js +30 -0
- package/templates/migrations/users_core_console_owner.cjs +39 -0
- package/templates/migrations/users_core_initial.cjs +118 -0
- package/templates/migrations/users_core_profile_username.cjs +98 -0
- package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
- package/test/authProfileSyncService.test.js +119 -0
- package/test/avatarService.test.js +114 -0
- package/test/avatarStorageService.test.js +61 -0
- package/test/consoleService.test.js +57 -0
- package/test/consoleSettingsService.test.js +86 -0
- package/test/exportsContract.test.js +38 -0
- package/test/registerAvatarMultipartSupport.test.js +64 -0
- package/test/registerServiceRealtimeEvents.test.js +160 -0
- package/test/registerWorkspaceDirectory.test.js +26 -0
- package/test/registerWorkspaceSettings.test.js +44 -0
- package/test/resourcesCanonical.test.js +90 -0
- package/test/roles.test.js +74 -0
- package/test/settingsFieldRegistriesSingleton.test.js +24 -0
- package/test/tenancyProfile.test.js +67 -0
- package/test/userSettingsResource.test.js +31 -0
- package/test/usersApiPaths.test.js +31 -0
- package/test/usersRouteRequestInputValidator.test.js +556 -0
- package/test/usersRouteResources.test.js +113 -0
- package/test/usersRouteValidators.test.js +49 -0
- package/test/usersVisibility.test.js +22 -0
- package/test/workspaceActionContextContributor.test.js +251 -0
- package/test/workspaceActionSurfaces.test.js +105 -0
- package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
- package/test/workspaceBootstrapContributor.test.js +466 -0
- package/test/workspaceInvitationsPolicy.test.js +71 -0
- package/test/workspaceInvitesRepository.test.js +111 -0
- package/test/workspaceMembersService.test.js +400 -0
- package/test/workspacePathModel.test.js +93 -0
- package/test/workspacePendingInvitationsResource.test.js +38 -0
- package/test/workspacePendingInvitationsService.test.js +151 -0
- package/test/workspaceRouteVisibilityResolver.test.js +83 -0
- package/test/workspaceService.test.js +480 -0
- package/test/workspaceSettingsActions.test.js +42 -0
- package/test/workspaceSettingsRepository.test.js +156 -0
- package/test/workspaceSettingsResource.test.js +156 -0
- package/test/workspaceSettingsService.test.js +120 -0
- package/test-support/registerDefaultSettingsFields.js +3 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
4
|
+
import { UsersCoreServiceProvider } from "../src/server/UsersCoreServiceProvider.js";
|
|
5
|
+
import { resolveTenancyProfile } from "../src/shared/tenancyProfile.js";
|
|
6
|
+
|
|
7
|
+
function createReplyDouble() {
|
|
8
|
+
return {
|
|
9
|
+
statusCode: 200,
|
|
10
|
+
payload: null,
|
|
11
|
+
redirectedTo: "",
|
|
12
|
+
code(value) {
|
|
13
|
+
this.statusCode = value;
|
|
14
|
+
return this;
|
|
15
|
+
},
|
|
16
|
+
send(value) {
|
|
17
|
+
this.payload = value;
|
|
18
|
+
return this;
|
|
19
|
+
},
|
|
20
|
+
redirect(value) {
|
|
21
|
+
this.redirectedTo = String(value || "");
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findRoute(routes, { method, path }) {
|
|
28
|
+
return routes.find((route) => route.method === method && route.path === path) || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function registerRoutes({
|
|
32
|
+
authService = {},
|
|
33
|
+
consoleService = null,
|
|
34
|
+
workspaceEnabled = true,
|
|
35
|
+
workspaceTenancyEnabled = true,
|
|
36
|
+
workspaceInvitationsEnabled = true,
|
|
37
|
+
workspaceSelfCreateEnabled = true
|
|
38
|
+
} = {}) {
|
|
39
|
+
const registeredRoutes = [];
|
|
40
|
+
const router = {
|
|
41
|
+
register(method, path, route, handler) {
|
|
42
|
+
registeredRoutes.push({
|
|
43
|
+
...route,
|
|
44
|
+
method,
|
|
45
|
+
path,
|
|
46
|
+
handler
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const bindings = new Map([
|
|
52
|
+
[KERNEL_TOKENS.HttpRouter, router],
|
|
53
|
+
["authService", authService],
|
|
54
|
+
[
|
|
55
|
+
"users.accountProfile.service",
|
|
56
|
+
{
|
|
57
|
+
async readAvatar() {
|
|
58
|
+
return {
|
|
59
|
+
mimeType: "image/png",
|
|
60
|
+
buffer: Buffer.from([])
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
["actionExecutor", {}],
|
|
66
|
+
["users.workspace.enabled", workspaceEnabled],
|
|
67
|
+
["users.workspace.tenancy.enabled", workspaceTenancyEnabled],
|
|
68
|
+
["users.workspace.invitations.enabled", workspaceInvitationsEnabled],
|
|
69
|
+
["users.workspace.self-create.enabled", workspaceSelfCreateEnabled]
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
if (consoleService) {
|
|
73
|
+
bindings.set("consoleService", consoleService);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const app = {
|
|
77
|
+
has(token) {
|
|
78
|
+
return bindings.has(token);
|
|
79
|
+
},
|
|
80
|
+
make(token) {
|
|
81
|
+
if (!bindings.has(token)) {
|
|
82
|
+
throw new Error(`Missing test binding for token: ${String(token)}`);
|
|
83
|
+
}
|
|
84
|
+
return bindings.get(token);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const provider = new UsersCoreServiceProvider();
|
|
89
|
+
await provider.boot(app);
|
|
90
|
+
|
|
91
|
+
return registeredRoutes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function registerRoutesForMode({
|
|
95
|
+
tenancyMode = "none",
|
|
96
|
+
tenancyPolicy = {}
|
|
97
|
+
} = {}) {
|
|
98
|
+
const tenancyProfile = resolveTenancyProfile({
|
|
99
|
+
tenancyMode,
|
|
100
|
+
tenancyPolicy
|
|
101
|
+
});
|
|
102
|
+
return registerRoutes({
|
|
103
|
+
workspaceEnabled: tenancyProfile.workspace.enabled === true,
|
|
104
|
+
workspaceTenancyEnabled: tenancyProfile.mode === "workspace",
|
|
105
|
+
workspaceInvitationsEnabled:
|
|
106
|
+
tenancyProfile.workspace.enabled === true && tenancyProfile.mode !== "none",
|
|
107
|
+
workspaceSelfCreateEnabled: tenancyProfile.workspace.allowSelfCreate === true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createActionRequest({ input = {}, executeAction, file = null }) {
|
|
112
|
+
return {
|
|
113
|
+
input,
|
|
114
|
+
executeAction,
|
|
115
|
+
file,
|
|
116
|
+
user: {
|
|
117
|
+
id: 42
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test("workspace and settings routes attach only the shared transport normalizers they actually use", async () => {
|
|
123
|
+
const routes = await registerRoutes();
|
|
124
|
+
|
|
125
|
+
const workspaceSettings = findRoute(routes, {
|
|
126
|
+
method: "GET",
|
|
127
|
+
path: "/api/w/:workspaceSlug/workspace/settings"
|
|
128
|
+
});
|
|
129
|
+
const workspaceSettingsPatch = findRoute(routes, {
|
|
130
|
+
method: "PATCH",
|
|
131
|
+
path: "/api/w/:workspaceSlug/workspace/settings"
|
|
132
|
+
});
|
|
133
|
+
const workspaceMemberRole = findRoute(routes, {
|
|
134
|
+
method: "PATCH",
|
|
135
|
+
path: "/api/w/:workspaceSlug/workspace/members/:memberUserId/role"
|
|
136
|
+
});
|
|
137
|
+
const workspaceMemberDelete = findRoute(routes, {
|
|
138
|
+
method: "DELETE",
|
|
139
|
+
path: "/api/w/:workspaceSlug/workspace/members/:memberUserId"
|
|
140
|
+
});
|
|
141
|
+
const workspaceInviteDelete = findRoute(routes, {
|
|
142
|
+
method: "DELETE",
|
|
143
|
+
path: "/api/w/:workspaceSlug/workspace/invites/:inviteId"
|
|
144
|
+
});
|
|
145
|
+
const settingsProfilePatch = findRoute(routes, {
|
|
146
|
+
method: "PATCH",
|
|
147
|
+
path: "/api/settings/profile"
|
|
148
|
+
});
|
|
149
|
+
const settingsOAuthStart = findRoute(routes, {
|
|
150
|
+
method: "GET",
|
|
151
|
+
path: "/api/settings/security/oauth/:provider/start"
|
|
152
|
+
});
|
|
153
|
+
const consoleSettingsPatch = findRoute(routes, {
|
|
154
|
+
method: "PATCH",
|
|
155
|
+
path: "/api/console/settings"
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
assert.equal(typeof workspaceSettings?.paramsValidator?.normalize, "function");
|
|
159
|
+
assert.equal(typeof workspaceSettingsPatch?.bodyValidator?.normalize, "function");
|
|
160
|
+
assert.equal(typeof workspaceMemberRole?.paramsValidator?.normalize, "function");
|
|
161
|
+
assert.equal(typeof workspaceMemberRole?.bodyValidator?.normalize, "function");
|
|
162
|
+
assert.equal(typeof workspaceMemberDelete?.paramsValidator?.normalize, "function");
|
|
163
|
+
assert.equal(typeof workspaceInviteDelete?.paramsValidator?.normalize, "function");
|
|
164
|
+
assert.equal(typeof settingsProfilePatch?.bodyValidator?.normalize, "function");
|
|
165
|
+
assert.equal(typeof settingsOAuthStart?.paramsValidator?.normalize, "function");
|
|
166
|
+
assert.equal(typeof settingsOAuthStart?.queryValidator?.normalize, "function");
|
|
167
|
+
assert.equal(typeof consoleSettingsPatch?.bodyValidator?.normalize, "function");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("workspace settings routes mount one canonical workspace endpoint", async () => {
|
|
171
|
+
const routes = await registerRoutes();
|
|
172
|
+
const workspaceSettings = findRoute(routes, {
|
|
173
|
+
method: "GET",
|
|
174
|
+
path: "/api/w/:workspaceSlug/workspace/settings"
|
|
175
|
+
});
|
|
176
|
+
const workspaceSettingsPatch = findRoute(routes, {
|
|
177
|
+
method: "PATCH",
|
|
178
|
+
path: "/api/w/:workspaceSlug/workspace/settings"
|
|
179
|
+
});
|
|
180
|
+
const adminWorkspaceSettings = findRoute(routes, {
|
|
181
|
+
method: "GET",
|
|
182
|
+
path: "/api/admin/w/:workspaceSlug/workspace/settings"
|
|
183
|
+
});
|
|
184
|
+
const consoleWorkspaceSettings = findRoute(routes, {
|
|
185
|
+
method: "GET",
|
|
186
|
+
path: "/api/console/w/:workspaceSlug/workspace/settings"
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
assert.ok(workspaceSettings);
|
|
190
|
+
assert.equal(workspaceSettings?.visibility, "workspace");
|
|
191
|
+
assert.equal(workspaceSettingsPatch?.visibility, "workspace");
|
|
192
|
+
assert.equal(workspaceSettings?.surface, "");
|
|
193
|
+
assert.equal(workspaceSettingsPatch?.surface, "");
|
|
194
|
+
assert.equal(adminWorkspaceSettings, null);
|
|
195
|
+
assert.equal(consoleWorkspaceSettings, null);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("users-core boot skips workspace routes when workspace policy is disabled", async () => {
|
|
199
|
+
const routes = await registerRoutes({
|
|
200
|
+
workspaceEnabled: false,
|
|
201
|
+
workspaceTenancyEnabled: false,
|
|
202
|
+
workspaceInvitationsEnabled: false,
|
|
203
|
+
workspaceSelfCreateEnabled: false
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
assert.equal(findRoute(routes, { method: "GET", path: "/api/workspaces" }), null);
|
|
207
|
+
assert.equal(findRoute(routes, { method: "POST", path: "/api/workspaces" }), null);
|
|
208
|
+
assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" }), null);
|
|
209
|
+
assert.equal(findRoute(routes, { method: "GET", path: "/api/settings" })?.path, "/api/settings");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("users-core boot skips workspace create route when self-create policy is disabled", async () => {
|
|
213
|
+
const routes = await registerRoutes({
|
|
214
|
+
workspaceEnabled: true,
|
|
215
|
+
workspaceTenancyEnabled: true,
|
|
216
|
+
workspaceInvitationsEnabled: true,
|
|
217
|
+
workspaceSelfCreateEnabled: false
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
assert.equal(findRoute(routes, { method: "POST", path: "/api/workspaces" }), null);
|
|
221
|
+
assert.equal(findRoute(routes, { method: "GET", path: "/api/workspaces" })?.path, "/api/workspaces");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("users-core route registration follows tenancy mode matrix", async () => {
|
|
225
|
+
const noneRoutes = await registerRoutesForMode({
|
|
226
|
+
tenancyMode: "none"
|
|
227
|
+
});
|
|
228
|
+
const personalRoutes = await registerRoutesForMode({
|
|
229
|
+
tenancyMode: "personal"
|
|
230
|
+
});
|
|
231
|
+
const workspaceRoutes = await registerRoutesForMode({
|
|
232
|
+
tenancyMode: "workspace"
|
|
233
|
+
});
|
|
234
|
+
const workspaceSelfCreateRoutes = await registerRoutesForMode({
|
|
235
|
+
tenancyMode: "workspace",
|
|
236
|
+
tenancyPolicy: {
|
|
237
|
+
workspace: {
|
|
238
|
+
allowSelfCreate: true
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/workspaces" }), null);
|
|
244
|
+
assert.equal(findRoute(noneRoutes, { method: "POST", path: "/api/workspaces" }), null);
|
|
245
|
+
assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" }), null);
|
|
246
|
+
assert.equal(findRoute(noneRoutes, { method: "GET", path: "/api/workspace/invitations/pending" }), null);
|
|
247
|
+
|
|
248
|
+
assert.equal(findRoute(personalRoutes, { method: "GET", path: "/api/workspaces" })?.path, "/api/workspaces");
|
|
249
|
+
assert.equal(findRoute(personalRoutes, { method: "POST", path: "/api/workspaces" }), null);
|
|
250
|
+
assert.equal(
|
|
251
|
+
findRoute(personalRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" })?.path,
|
|
252
|
+
"/api/w/:workspaceSlug/workspace/settings"
|
|
253
|
+
);
|
|
254
|
+
assert.equal(
|
|
255
|
+
findRoute(personalRoutes, { method: "GET", path: "/api/workspace/invitations/pending" })?.path,
|
|
256
|
+
"/api/workspace/invitations/pending"
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
assert.equal(findRoute(workspaceRoutes, { method: "GET", path: "/api/workspaces" })?.path, "/api/workspaces");
|
|
260
|
+
assert.equal(findRoute(workspaceRoutes, { method: "POST", path: "/api/workspaces" }), null);
|
|
261
|
+
assert.equal(
|
|
262
|
+
findRoute(workspaceRoutes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/settings" })?.path,
|
|
263
|
+
"/api/w/:workspaceSlug/workspace/settings"
|
|
264
|
+
);
|
|
265
|
+
assert.equal(
|
|
266
|
+
findRoute(workspaceRoutes, { method: "GET", path: "/api/workspace/invitations/pending" })?.path,
|
|
267
|
+
"/api/workspace/invitations/pending"
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
assert.equal(
|
|
271
|
+
findRoute(workspaceSelfCreateRoutes, { method: "POST", path: "/api/workspaces" })?.path,
|
|
272
|
+
"/api/workspaces"
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("users-core boot skips invitation redeem/list routes when workspace invitations are disabled", async () => {
|
|
277
|
+
const routes = await registerRoutes({
|
|
278
|
+
workspaceEnabled: true,
|
|
279
|
+
workspaceTenancyEnabled: true,
|
|
280
|
+
workspaceInvitationsEnabled: false,
|
|
281
|
+
workspaceSelfCreateEnabled: false
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
assert.equal(findRoute(routes, { method: "GET", path: "/api/workspace/invitations/pending" }), null);
|
|
285
|
+
assert.equal(findRoute(routes, { method: "POST", path: "/api/workspace/invitations/redeem" }), null);
|
|
286
|
+
assert.equal(findRoute(routes, { method: "GET", path: "/api/w/:workspaceSlug/workspace/invites" }), null);
|
|
287
|
+
assert.equal(findRoute(routes, { method: "POST", path: "/api/w/:workspaceSlug/workspace/invites" }), null);
|
|
288
|
+
assert.equal(findRoute(routes, { method: "DELETE", path: "/api/w/:workspaceSlug/workspace/invites/:inviteId" }), null);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("workspace invite and member handlers build action input from request.input", async () => {
|
|
292
|
+
const routes = await registerRoutes();
|
|
293
|
+
const workspaceCreate = findRoute(routes, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
path: "/api/workspaces"
|
|
296
|
+
});
|
|
297
|
+
const workspaceInviteRedeem = findRoute(routes, {
|
|
298
|
+
method: "POST",
|
|
299
|
+
path: "/api/workspace/invitations/redeem"
|
|
300
|
+
});
|
|
301
|
+
const workspaceMemberRolePatch = findRoute(routes, {
|
|
302
|
+
method: "PATCH",
|
|
303
|
+
path: "/api/w/:workspaceSlug/workspace/members/:memberUserId/role"
|
|
304
|
+
});
|
|
305
|
+
const workspaceMemberDelete = findRoute(routes, {
|
|
306
|
+
method: "DELETE",
|
|
307
|
+
path: "/api/w/:workspaceSlug/workspace/members/:memberUserId"
|
|
308
|
+
});
|
|
309
|
+
const workspaceInviteCreate = findRoute(routes, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
path: "/api/w/:workspaceSlug/workspace/invites"
|
|
312
|
+
});
|
|
313
|
+
const workspaceInviteDelete = findRoute(routes, {
|
|
314
|
+
method: "DELETE",
|
|
315
|
+
path: "/api/w/:workspaceSlug/workspace/invites/:inviteId"
|
|
316
|
+
});
|
|
317
|
+
const calls = [];
|
|
318
|
+
const executeAction = async (payload) => {
|
|
319
|
+
calls.push(payload);
|
|
320
|
+
return {};
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
await workspaceCreate.handler(
|
|
324
|
+
createActionRequest({
|
|
325
|
+
input: {
|
|
326
|
+
body: { name: "Operations", slug: "operations" }
|
|
327
|
+
},
|
|
328
|
+
executeAction
|
|
329
|
+
}),
|
|
330
|
+
createReplyDouble()
|
|
331
|
+
);
|
|
332
|
+
await workspaceInviteRedeem.handler(
|
|
333
|
+
createActionRequest({
|
|
334
|
+
input: {
|
|
335
|
+
body: { token: "token-1", decision: "accept" }
|
|
336
|
+
},
|
|
337
|
+
executeAction
|
|
338
|
+
}),
|
|
339
|
+
createReplyDouble()
|
|
340
|
+
);
|
|
341
|
+
await workspaceMemberRolePatch.handler(
|
|
342
|
+
createActionRequest({
|
|
343
|
+
input: {
|
|
344
|
+
params: { workspaceSlug: "acme", memberUserId: "12" },
|
|
345
|
+
body: { roleId: "admin" }
|
|
346
|
+
},
|
|
347
|
+
executeAction
|
|
348
|
+
}),
|
|
349
|
+
createReplyDouble()
|
|
350
|
+
);
|
|
351
|
+
await workspaceInviteCreate.handler(
|
|
352
|
+
createActionRequest({
|
|
353
|
+
input: {
|
|
354
|
+
params: { workspaceSlug: "acme" },
|
|
355
|
+
body: { email: "user@example.com", roleId: "member" }
|
|
356
|
+
},
|
|
357
|
+
executeAction
|
|
358
|
+
}),
|
|
359
|
+
createReplyDouble()
|
|
360
|
+
);
|
|
361
|
+
await workspaceMemberDelete.handler(
|
|
362
|
+
createActionRequest({
|
|
363
|
+
input: {
|
|
364
|
+
params: { workspaceSlug: "acme", memberUserId: "44" }
|
|
365
|
+
},
|
|
366
|
+
executeAction
|
|
367
|
+
}),
|
|
368
|
+
createReplyDouble()
|
|
369
|
+
);
|
|
370
|
+
await workspaceInviteDelete.handler(
|
|
371
|
+
createActionRequest({
|
|
372
|
+
input: {
|
|
373
|
+
params: { workspaceSlug: "acme", inviteId: "55" }
|
|
374
|
+
},
|
|
375
|
+
executeAction
|
|
376
|
+
}),
|
|
377
|
+
createReplyDouble()
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
assert.deepEqual(calls[0], {
|
|
381
|
+
actionId: "workspace.workspaces.create",
|
|
382
|
+
input: { name: "Operations", slug: "operations" }
|
|
383
|
+
});
|
|
384
|
+
assert.deepEqual(calls[1].input, { payload: { token: "token-1", decision: "accept" } });
|
|
385
|
+
assert.deepEqual(calls[2].input, { workspaceSlug: "acme", memberUserId: "12", roleId: "admin" });
|
|
386
|
+
assert.deepEqual(calls[3].input, { workspaceSlug: "acme", email: "user@example.com", roleId: "member" });
|
|
387
|
+
assert.deepEqual(calls[4].input, { workspaceSlug: "acme", memberUserId: "44" });
|
|
388
|
+
assert.deepEqual(calls[5].input, { workspaceSlug: "acme", inviteId: "55" });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("workspace settings route handlers build action input from request.input", async () => {
|
|
392
|
+
const routes = await registerRoutes();
|
|
393
|
+
const workspaceSettingsPatch = findRoute(routes, {
|
|
394
|
+
method: "PATCH",
|
|
395
|
+
path: "/api/w/:workspaceSlug/workspace/settings"
|
|
396
|
+
});
|
|
397
|
+
const calls = [];
|
|
398
|
+
const executeAction = async (payload) => {
|
|
399
|
+
calls.push(payload);
|
|
400
|
+
return {};
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
await workspaceSettingsPatch.handler(
|
|
404
|
+
createActionRequest({
|
|
405
|
+
input: {
|
|
406
|
+
params: { workspaceSlug: "acme" },
|
|
407
|
+
body: { name: "Acme Workspace" }
|
|
408
|
+
},
|
|
409
|
+
executeAction
|
|
410
|
+
}),
|
|
411
|
+
createReplyDouble()
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
assert.deepEqual(calls[0], {
|
|
415
|
+
actionId: "workspace.settings.update",
|
|
416
|
+
input: { workspaceSlug: "acme", patch: { name: "Acme Workspace" } }
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("account route handlers build action input from request.input", async () => {
|
|
421
|
+
const routes = await registerRoutes({
|
|
422
|
+
authService: {
|
|
423
|
+
writeSessionCookies() {}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
const calls = [];
|
|
427
|
+
const executeAction = async (payload) => {
|
|
428
|
+
calls.push(payload);
|
|
429
|
+
if (payload.actionId === "settings.security.oauth.link.start") {
|
|
430
|
+
return { url: "/oauth/link" };
|
|
431
|
+
}
|
|
432
|
+
if (payload.actionId === "settings.profile.update") {
|
|
433
|
+
return { settings: {}, session: null };
|
|
434
|
+
}
|
|
435
|
+
if (payload.actionId === "settings.security.password.change") {
|
|
436
|
+
return { message: "ok", session: null };
|
|
437
|
+
}
|
|
438
|
+
return {};
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
await findRoute(routes, { method: "PATCH", path: "/api/settings/profile" }).handler(
|
|
442
|
+
createActionRequest({
|
|
443
|
+
input: { body: { displayName: "Merc" } },
|
|
444
|
+
executeAction
|
|
445
|
+
}),
|
|
446
|
+
createReplyDouble()
|
|
447
|
+
);
|
|
448
|
+
await findRoute(routes, { method: "PATCH", path: "/api/settings/preferences" }).handler(
|
|
449
|
+
createActionRequest({
|
|
450
|
+
input: { body: { locale: "en-US" } },
|
|
451
|
+
executeAction
|
|
452
|
+
}),
|
|
453
|
+
createReplyDouble()
|
|
454
|
+
);
|
|
455
|
+
await findRoute(routes, { method: "PATCH", path: "/api/settings/notifications" }).handler(
|
|
456
|
+
createActionRequest({
|
|
457
|
+
input: { body: { email: true } },
|
|
458
|
+
executeAction
|
|
459
|
+
}),
|
|
460
|
+
createReplyDouble()
|
|
461
|
+
);
|
|
462
|
+
await findRoute(routes, { method: "POST", path: "/api/settings/security/change-password" }).handler(
|
|
463
|
+
createActionRequest({
|
|
464
|
+
input: {
|
|
465
|
+
body: {
|
|
466
|
+
currentPassword: "old-password",
|
|
467
|
+
newPassword: "new-password-123",
|
|
468
|
+
confirmPassword: "new-password-123"
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
executeAction
|
|
472
|
+
}),
|
|
473
|
+
createReplyDouble()
|
|
474
|
+
);
|
|
475
|
+
await findRoute(routes, { method: "PATCH", path: "/api/settings/security/methods/password" }).handler(
|
|
476
|
+
createActionRequest({
|
|
477
|
+
input: { body: { enabled: true } },
|
|
478
|
+
executeAction
|
|
479
|
+
}),
|
|
480
|
+
createReplyDouble()
|
|
481
|
+
);
|
|
482
|
+
const oauthReply = createReplyDouble();
|
|
483
|
+
await findRoute(routes, { method: "GET", path: "/api/settings/security/oauth/:provider/start" }).handler(
|
|
484
|
+
createActionRequest({
|
|
485
|
+
input: {
|
|
486
|
+
params: { provider: "github" },
|
|
487
|
+
query: { returnTo: "/app/settings" }
|
|
488
|
+
},
|
|
489
|
+
executeAction
|
|
490
|
+
}),
|
|
491
|
+
oauthReply
|
|
492
|
+
);
|
|
493
|
+
await findRoute(routes, { method: "DELETE", path: "/api/settings/security/oauth/:provider" }).handler(
|
|
494
|
+
createActionRequest({
|
|
495
|
+
input: { params: { provider: "github" } },
|
|
496
|
+
executeAction
|
|
497
|
+
}),
|
|
498
|
+
createReplyDouble()
|
|
499
|
+
);
|
|
500
|
+
await findRoute(routes, { method: "POST", path: "/api/settings/security/logout-others" }).handler(
|
|
501
|
+
createActionRequest({
|
|
502
|
+
executeAction
|
|
503
|
+
}),
|
|
504
|
+
createReplyDouble()
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
assert.deepEqual(calls[0].input, { payload: { displayName: "Merc" } });
|
|
508
|
+
assert.deepEqual(calls[1].input, { payload: { locale: "en-US" } });
|
|
509
|
+
assert.deepEqual(calls[2].input, { payload: { email: true } });
|
|
510
|
+
assert.deepEqual(calls[3].input, {
|
|
511
|
+
payload: {
|
|
512
|
+
currentPassword: "old-password",
|
|
513
|
+
newPassword: "new-password-123",
|
|
514
|
+
confirmPassword: "new-password-123"
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
assert.deepEqual(calls[4].input, { payload: { enabled: true } });
|
|
518
|
+
assert.deepEqual(calls[5].input, { provider: "github", returnTo: "/app/settings" });
|
|
519
|
+
assert.equal(oauthReply.redirectedTo, "/oauth/link");
|
|
520
|
+
assert.deepEqual(calls[6].input, { provider: "github" });
|
|
521
|
+
assert.equal(calls[7].actionId, "settings.security.sessions.logout_others");
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("console settings route handlers use request.input payloads", async () => {
|
|
525
|
+
const routes = await registerRoutes();
|
|
526
|
+
const calls = [];
|
|
527
|
+
const executeAction = async (payload) => {
|
|
528
|
+
calls.push(payload);
|
|
529
|
+
return {
|
|
530
|
+
settings: {}
|
|
531
|
+
};
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
await findRoute(routes, { method: "GET", path: "/api/console/settings" }).handler(
|
|
535
|
+
createActionRequest({ executeAction }),
|
|
536
|
+
createReplyDouble()
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
await findRoute(routes, { method: "PATCH", path: "/api/console/settings" }).handler(
|
|
540
|
+
createActionRequest({
|
|
541
|
+
input: {
|
|
542
|
+
body: {}
|
|
543
|
+
},
|
|
544
|
+
executeAction
|
|
545
|
+
}),
|
|
546
|
+
createReplyDouble()
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
assert.equal(calls[0].actionId, "console.settings.read");
|
|
550
|
+
assert.deepEqual(calls[1], {
|
|
551
|
+
actionId: "console.settings.update",
|
|
552
|
+
input: {
|
|
553
|
+
payload: {}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { deriveResourceRequiredMetadata } from "@jskit-ai/kernel/_testable";
|
|
7
|
+
import "../test-support/registerDefaultSettingsFields.js";
|
|
8
|
+
import { consoleSettingsResource } from "../src/shared/resources/consoleSettingsResource.js";
|
|
9
|
+
import { userProfileResource } from "../src/shared/resources/userProfileResource.js";
|
|
10
|
+
import { userSettingsResource } from "../src/shared/resources/userSettingsResource.js";
|
|
11
|
+
import { workspaceMembersResource } from "../src/shared/resources/workspaceMembersResource.js";
|
|
12
|
+
import { workspaceResource } from "../src/shared/resources/workspaceResource.js";
|
|
13
|
+
import { workspaceSettingsResource } from "../src/shared/resources/workspaceSettingsResource.js";
|
|
14
|
+
|
|
15
|
+
function assertResourceShape(resource, label) {
|
|
16
|
+
assert.ok(resource, `${label} resource must exist.`);
|
|
17
|
+
assert.equal(typeof resource, "object", `${label} resource must be an object.`);
|
|
18
|
+
assert.equal(typeof resource.resource, "string", `${label}.resource must be a string.`);
|
|
19
|
+
|
|
20
|
+
for (const operationName of ["view", "list", "create", "replace", "patch"]) {
|
|
21
|
+
const operation = resource.operations?.[operationName];
|
|
22
|
+
assert.equal(typeof operation, "object", `${label}.operations.${operationName} must exist.`);
|
|
23
|
+
assert.equal(typeof operation.method, "string", `${label}.operations.${operationName}.method must exist.`);
|
|
24
|
+
const resolvedMessages =
|
|
25
|
+
operation?.messages && typeof operation.messages === "object"
|
|
26
|
+
? operation.messages
|
|
27
|
+
: resource?.messages || resource?.operationMessages;
|
|
28
|
+
assert.equal(
|
|
29
|
+
typeof resolvedMessages,
|
|
30
|
+
"object",
|
|
31
|
+
`${label}.operations.${operationName} must resolve messages from operation.messages or resource.messages.`
|
|
32
|
+
);
|
|
33
|
+
assert.equal(
|
|
34
|
+
typeof operation.outputValidator?.schema,
|
|
35
|
+
"object",
|
|
36
|
+
`${label}.operations.${operationName} payload schema is required.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
assert.equal(typeof resource.operations.create.bodyValidator?.schema, "object", `${label}.operations.create.bodyValidator.schema is required.`);
|
|
41
|
+
assert.equal(typeof resource.operations.replace.bodyValidator?.schema, "object", `${label}.operations.replace.bodyValidator.schema is required.`);
|
|
42
|
+
assert.equal(typeof resource.operations.patch.bodyValidator?.schema, "object", `${label}.operations.patch.bodyValidator.schema is required.`);
|
|
43
|
+
|
|
44
|
+
const requiredMetadata = deriveResourceRequiredMetadata(resource);
|
|
45
|
+
assert.ok(Array.isArray(requiredMetadata.create), `${label}.derivedRequired.create must be an array.`);
|
|
46
|
+
assert.ok(Array.isArray(requiredMetadata.replace), `${label}.derivedRequired.replace must be an array.`);
|
|
47
|
+
assert.ok(Array.isArray(requiredMetadata.patch), `${label}.derivedRequired.patch must be an array.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("workspace/settings/console resources expose canonical validators", () => {
|
|
51
|
+
const resourcesByLabel = {
|
|
52
|
+
workspace: workspaceResource,
|
|
53
|
+
workspaceSettings: workspaceSettingsResource,
|
|
54
|
+
userProfile: userProfileResource,
|
|
55
|
+
userSettings: userSettingsResource,
|
|
56
|
+
consoleSettings: consoleSettingsResource
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
for (const [label, resource] of Object.entries(resourcesByLabel)) {
|
|
60
|
+
assertResourceShape(resource, label);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("specialized settings and invite operations expose canonical validators", () => {
|
|
65
|
+
const workspaceMembersOperationSpecs = [
|
|
66
|
+
{ label: "workspaceMembers.rolesList", operation: workspaceMembersResource.operations.rolesList },
|
|
67
|
+
{ label: "workspaceMembers.membersList", operation: workspaceMembersResource.operations.membersList },
|
|
68
|
+
{ label: "workspaceMembers.updateMemberRole", operation: workspaceMembersResource.operations.updateMemberRole },
|
|
69
|
+
{ label: "workspaceMembers.removeMember", operation: workspaceMembersResource.operations.removeMember },
|
|
70
|
+
{ label: "workspaceMembers.invitesList", operation: workspaceMembersResource.operations.invitesList },
|
|
71
|
+
{ label: "workspaceMembers.createInvite", operation: workspaceMembersResource.operations.createInvite },
|
|
72
|
+
{ label: "workspaceMembers.revokeInvite", operation: workspaceMembersResource.operations.revokeInvite },
|
|
73
|
+
{ label: "workspaceMembers.redeemInvite", operation: workspaceMembersResource.operations.redeemInvite }
|
|
74
|
+
];
|
|
75
|
+
const operationSpecs = [
|
|
76
|
+
...workspaceMembersOperationSpecs,
|
|
77
|
+
{ label: "userProfile.avatarUpload", operation: userProfileResource.operations.avatarUpload },
|
|
78
|
+
{ label: "userProfile.avatarDelete", operation: userProfileResource.operations.avatarDelete },
|
|
79
|
+
{ label: "userSettings.passwordChange", operation: userSettingsResource.operations.passwordChange },
|
|
80
|
+
{ label: "userSettings.passwordMethodToggle", operation: userSettingsResource.operations.passwordMethodToggle },
|
|
81
|
+
{ label: "userSettings.oauthLinkStart", operation: userSettingsResource.operations.oauthLinkStart },
|
|
82
|
+
{ label: "userSettings.oauthUnlink", operation: userSettingsResource.operations.oauthUnlink },
|
|
83
|
+
{ label: "userSettings.logoutOtherSessions", operation: userSettingsResource.operations.logoutOtherSessions }
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (const { label, operation } of operationSpecs) {
|
|
87
|
+
assert.equal(typeof operation?.method, "string", `${label}.method must exist.`);
|
|
88
|
+
assert.equal(typeof operation?.outputValidator?.schema, "object", `${label}.outputValidator.schema must exist.`);
|
|
89
|
+
if (operation?.bodyValidator) {
|
|
90
|
+
assert.equal(typeof operation.bodyValidator.schema, "object", `${label}.bodyValidator.schema must exist.`);
|
|
91
|
+
}
|
|
92
|
+
if (operation?.paramsValidator) {
|
|
93
|
+
assert.equal(typeof operation.paramsValidator.schema, "object", `${label}.paramsValidator.schema must exist.`);
|
|
94
|
+
}
|
|
95
|
+
if (operation?.queryValidator) {
|
|
96
|
+
assert.equal(typeof operation.queryValidator.schema, "object", `${label}.queryValidator.schema must exist.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("users-core no longer uses a workspace schema helper that exposes raw schema leaves", () => {
|
|
102
|
+
const testFilePath = fileURLToPath(import.meta.url);
|
|
103
|
+
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
104
|
+
const legacyWorkspaceRoutesFile = path.join(packageRoot, "src", "server", "common", "routes", "workspaceRoutes.js");
|
|
105
|
+
assert.equal(existsSync(legacyWorkspaceRoutesFile), false, "workspaceRoutes.js must not exist.");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("users-core route validators no longer live under a legacy shared/schema directory", () => {
|
|
109
|
+
const testFilePath = fileURLToPath(import.meta.url);
|
|
110
|
+
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
111
|
+
const legacySchemaDir = path.join(packageRoot, "src", "shared", "schema");
|
|
112
|
+
assert.equal(existsSync(legacySchemaDir), false, "src/shared/schema must not exist.");
|
|
113
|
+
});
|