@jskit-ai/console-core 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.
- package/package.descriptor.mjs +119 -0
- package/package.json +18 -0
- package/src/server/ConsoleCoreServiceProvider.js +22 -0
- package/src/server/consoleBootstrapContributor.js +37 -0
- package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +63 -0
- package/src/server/consoleSettings/consoleService.js +36 -0
- package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
- package/src/server/consoleSettings/consoleSettingsRepository.js +119 -0
- package/src/server/consoleSettings/consoleSettingsService.js +40 -0
- package/src/server/consoleSettings/registerConsoleSettings.js +56 -0
- package/src/server/registerConsoleBootstrap.js +16 -0
- package/src/server/registerConsoleCore.js +17 -0
- package/src/server/support/consoleActionSurfaces.js +62 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/consoleSettingsFields.js +54 -0
- package/src/shared/resources/consoleSettingsResource.js +119 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/templates/migrations/console_core_generic_initial.cjs +27 -0
- package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
- package/test/bootstrapPayloadIntegration.test.js +86 -0
- package/test/consoleActionSurfaces.test.js +24 -0
- package/test/consoleBootstrapContributor.test.js +64 -0
- package/test/consoleRouteRequestInputValidator.test.js +119 -0
- package/test/consoleRouteResources.test.js +72 -0
- package/test/consoleService.test.js +57 -0
- package/test/consoleSettingsService.test.js +86 -0
- package/test/exportsContract.test.js +26 -0
- package/test/registerConsoleCore.test.js +38 -0
- package/test/registerServiceRealtimeEvents.test.js +38 -0
- package/test/repositoryContracts.test.js +28 -0
- package/test/settingsFieldRegistriesSingleton.test.js +14 -0
- package/test-support/registerDefaultSettingsFields.js +1 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
2
|
+
import { resolveGlobalArrayRegistry } from "./resolveGlobalArrayRegistry.js";
|
|
3
|
+
|
|
4
|
+
const consoleSettingsFields = resolveGlobalArrayRegistry("jskit.console-core.consoleSettingsFields");
|
|
5
|
+
|
|
6
|
+
function defineField(field = {}) {
|
|
7
|
+
const key = normalizeText(field.key);
|
|
8
|
+
if (!key) {
|
|
9
|
+
throw new TypeError("consoleSettingsFields.defineField requires field.key.");
|
|
10
|
+
}
|
|
11
|
+
if (consoleSettingsFields.some((entry) => entry.key === key)) {
|
|
12
|
+
throw new Error(`consoleSettingsFields.defineField duplicate key: ${key}`);
|
|
13
|
+
}
|
|
14
|
+
if (!field.inputSchema || typeof field.inputSchema !== "object") {
|
|
15
|
+
throw new TypeError(`consoleSettingsFields.defineField("${key}") requires inputSchema.`);
|
|
16
|
+
}
|
|
17
|
+
if (!field.outputSchema || typeof field.outputSchema !== "object") {
|
|
18
|
+
throw new TypeError(`consoleSettingsFields.defineField("${key}") requires outputSchema.`);
|
|
19
|
+
}
|
|
20
|
+
const dbColumn = normalizeText(field.dbColumn);
|
|
21
|
+
if (!dbColumn) {
|
|
22
|
+
throw new TypeError(`consoleSettingsFields.defineField("${key}") requires dbColumn.`);
|
|
23
|
+
}
|
|
24
|
+
if (typeof field.normalizeInput !== "function") {
|
|
25
|
+
throw new TypeError(`consoleSettingsFields.defineField("${key}") requires normalizeInput.`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof field.normalizeOutput !== "function") {
|
|
28
|
+
throw new TypeError(`consoleSettingsFields.defineField("${key}") requires normalizeOutput.`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof field.resolveDefault !== "function") {
|
|
31
|
+
throw new TypeError(`consoleSettingsFields.defineField("${key}") requires resolveDefault.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
consoleSettingsFields.push({
|
|
35
|
+
key,
|
|
36
|
+
dbColumn,
|
|
37
|
+
required: field.required !== false,
|
|
38
|
+
inputSchema: field.inputSchema,
|
|
39
|
+
outputSchema: field.outputSchema,
|
|
40
|
+
normalizeInput: field.normalizeInput,
|
|
41
|
+
normalizeOutput: field.normalizeOutput,
|
|
42
|
+
resolveDefault: field.resolveDefault
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resetConsoleSettingsFields() {
|
|
47
|
+
consoleSettingsFields.splice(0, consoleSettingsFields.length);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
defineField,
|
|
52
|
+
resetConsoleSettingsFields,
|
|
53
|
+
consoleSettingsFields
|
|
54
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { createOperationMessages } from "../operationMessages.js";
|
|
3
|
+
import {
|
|
4
|
+
createCursorListValidator,
|
|
5
|
+
normalizeObjectInput,
|
|
6
|
+
normalizeSettingsFieldInput,
|
|
7
|
+
normalizeSettingsFieldOutput
|
|
8
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
9
|
+
import { consoleSettingsFields } from "./consoleSettingsFields.js";
|
|
10
|
+
|
|
11
|
+
function buildCreateSchema() {
|
|
12
|
+
const properties = {};
|
|
13
|
+
for (const field of consoleSettingsFields) {
|
|
14
|
+
properties[field.key] = field.required === false ? Type.Optional(field.inputSchema) : field.inputSchema;
|
|
15
|
+
}
|
|
16
|
+
return Type.Object(properties, { additionalProperties: false });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildOutputSchema() {
|
|
20
|
+
const properties = {};
|
|
21
|
+
for (const field of consoleSettingsFields) {
|
|
22
|
+
properties[field.key] = field.outputSchema;
|
|
23
|
+
}
|
|
24
|
+
return Type.Object(properties, { additionalProperties: false });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildConsoleSettingsRecordSchema() {
|
|
28
|
+
return Type.Object(
|
|
29
|
+
{
|
|
30
|
+
settings: buildOutputSchema()
|
|
31
|
+
},
|
|
32
|
+
{ additionalProperties: false }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildConsoleSettingsCreateSchema() {
|
|
37
|
+
return buildCreateSchema();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildConsoleSettingsReplaceSchema() {
|
|
41
|
+
return buildConsoleSettingsCreateSchema();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildConsoleSettingsPatchSchema() {
|
|
45
|
+
return Type.Partial(buildConsoleSettingsCreateSchema(), {
|
|
46
|
+
additionalProperties: false
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeConsoleSettingsInput(payload = {}) {
|
|
51
|
+
return normalizeSettingsFieldInput(payload, consoleSettingsFields);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const consoleSettingsOutputValidator = Object.freeze({
|
|
55
|
+
get schema() {
|
|
56
|
+
return buildConsoleSettingsRecordSchema();
|
|
57
|
+
},
|
|
58
|
+
normalize(payload = {}) {
|
|
59
|
+
const source = normalizeObjectInput(payload);
|
|
60
|
+
const settingsSource = normalizeObjectInput(source.settings);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
settings: normalizeSettingsFieldOutput(settingsSource, consoleSettingsFields)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const CONSOLE_SETTINGS_OPERATION_MESSAGES = createOperationMessages();
|
|
69
|
+
|
|
70
|
+
const consoleSettingsResource = Object.freeze({
|
|
71
|
+
resource: "consoleSettings",
|
|
72
|
+
operations: Object.freeze({
|
|
73
|
+
view: Object.freeze({
|
|
74
|
+
method: "GET",
|
|
75
|
+
messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
|
|
76
|
+
outputValidator: consoleSettingsOutputValidator
|
|
77
|
+
}),
|
|
78
|
+
list: Object.freeze({
|
|
79
|
+
method: "GET",
|
|
80
|
+
messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
|
|
81
|
+
outputValidator: createCursorListValidator(consoleSettingsOutputValidator)
|
|
82
|
+
}),
|
|
83
|
+
create: Object.freeze({
|
|
84
|
+
method: "POST",
|
|
85
|
+
messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
|
|
86
|
+
bodyValidator: Object.freeze({
|
|
87
|
+
get schema() {
|
|
88
|
+
return buildConsoleSettingsCreateSchema();
|
|
89
|
+
},
|
|
90
|
+
normalize: normalizeConsoleSettingsInput
|
|
91
|
+
}),
|
|
92
|
+
outputValidator: consoleSettingsOutputValidator
|
|
93
|
+
}),
|
|
94
|
+
replace: Object.freeze({
|
|
95
|
+
method: "PUT",
|
|
96
|
+
messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
|
|
97
|
+
bodyValidator: Object.freeze({
|
|
98
|
+
get schema() {
|
|
99
|
+
return buildConsoleSettingsReplaceSchema();
|
|
100
|
+
},
|
|
101
|
+
normalize: normalizeConsoleSettingsInput
|
|
102
|
+
}),
|
|
103
|
+
outputValidator: consoleSettingsOutputValidator
|
|
104
|
+
}),
|
|
105
|
+
patch: Object.freeze({
|
|
106
|
+
method: "PATCH",
|
|
107
|
+
messages: CONSOLE_SETTINGS_OPERATION_MESSAGES,
|
|
108
|
+
bodyValidator: Object.freeze({
|
|
109
|
+
get schema() {
|
|
110
|
+
return buildConsoleSettingsPatchSchema();
|
|
111
|
+
},
|
|
112
|
+
normalize: normalizeConsoleSettingsInput
|
|
113
|
+
}),
|
|
114
|
+
outputValidator: consoleSettingsOutputValidator
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export { consoleSettingsResource };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('knex').Knex} knex
|
|
3
|
+
*/
|
|
4
|
+
exports.up = async function up(knex) {
|
|
5
|
+
const hasConsoleSettingsTable = await knex.schema.hasTable("console_settings");
|
|
6
|
+
if (!hasConsoleSettingsTable) {
|
|
7
|
+
await knex.schema.createTable("console_settings", (table) => {
|
|
8
|
+
table.bigInteger("id").primary();
|
|
9
|
+
table.bigInteger("owner_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
|
|
10
|
+
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
11
|
+
table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
await knex("console_settings").insert({
|
|
15
|
+
id: 1,
|
|
16
|
+
created_at: knex.fn.now(),
|
|
17
|
+
updated_at: knex.fn.now()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('knex').Knex} knex
|
|
24
|
+
*/
|
|
25
|
+
exports.down = async function down(knex) {
|
|
26
|
+
await knex.schema.dropTableIfExists("console_settings");
|
|
27
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @jskit-contract console.settings-fields.v1
|
|
2
|
+
// Append-only settings field registrations for console settings.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
defineField,
|
|
6
|
+
resetConsoleSettingsFields
|
|
7
|
+
} from "@jskit-ai/console-core/shared/resources/consoleSettingsFields";
|
|
8
|
+
|
|
9
|
+
resetConsoleSettingsFields();
|
|
10
|
+
|
|
11
|
+
void defineField;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createContainer } from "../../kernel/server/container/index.js";
|
|
4
|
+
import { resolveBootstrapPayload } from "../../kernel/server/registries/bootstrapPayloadContributorRegistry.js";
|
|
5
|
+
import { registerUsersBootstrap } from "../../users-core/src/server/registerUsersBootstrap.js";
|
|
6
|
+
import { registerConsoleBootstrap } from "../src/server/registerConsoleBootstrap.js";
|
|
7
|
+
|
|
8
|
+
function createAuthenticatedProfile(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
id: "12",
|
|
11
|
+
authProvider: "local",
|
|
12
|
+
authProviderUserSid: "user-12",
|
|
13
|
+
username: "consoleowner",
|
|
14
|
+
displayName: "Console Owner",
|
|
15
|
+
email: "owner@example.com",
|
|
16
|
+
...overrides
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("bootstrap payload preserves consoleowner for authenticated users after users bootstrap runs", async () => {
|
|
21
|
+
const profile = createAuthenticatedProfile();
|
|
22
|
+
const ownerSeeds = [];
|
|
23
|
+
const app = createContainer();
|
|
24
|
+
|
|
25
|
+
app.instance("usersRepository", {
|
|
26
|
+
async findById(userId) {
|
|
27
|
+
return String(userId || "") === profile.id ? profile : null;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
app.instance("userSettingsRepository", {
|
|
31
|
+
async ensureForUserId() {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
app.instance("users.tenancy.profile", {
|
|
36
|
+
mode: "none",
|
|
37
|
+
workspace: {
|
|
38
|
+
enabled: false,
|
|
39
|
+
autoProvision: false,
|
|
40
|
+
allowSelfCreate: false,
|
|
41
|
+
slugPolicy: "none"
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
app.instance("authService", {
|
|
45
|
+
getOAuthProviderCatalog() {
|
|
46
|
+
return {
|
|
47
|
+
providers: [],
|
|
48
|
+
defaultProvider: null
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
writeSessionCookies() {},
|
|
52
|
+
clearSessionCookies() {}
|
|
53
|
+
});
|
|
54
|
+
app.instance("consoleService", {
|
|
55
|
+
async ensureInitialConsoleMember(userId) {
|
|
56
|
+
ownerSeeds.push(String(userId || ""));
|
|
57
|
+
return String(userId || "");
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
registerConsoleBootstrap(app);
|
|
62
|
+
registerUsersBootstrap(app);
|
|
63
|
+
|
|
64
|
+
const payload = await resolveBootstrapPayload(app, {
|
|
65
|
+
request: {
|
|
66
|
+
async executeAction({ actionId }) {
|
|
67
|
+
assert.equal(actionId, "auth.session.read");
|
|
68
|
+
return {
|
|
69
|
+
authenticated: true,
|
|
70
|
+
profile,
|
|
71
|
+
session: {
|
|
72
|
+
csrfToken: "csrf-1"
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
reply: {}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(ownerSeeds, ["12"]);
|
|
81
|
+
assert.equal(payload.session.authenticated, true);
|
|
82
|
+
assert.equal(payload.session.userId, "12");
|
|
83
|
+
assert.deepEqual(payload.surfaceAccess, {
|
|
84
|
+
consoleowner: true
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
resolveConsoleSurfaceIdsFromAppConfig
|
|
5
|
+
} from "../src/server/support/consoleActionSurfaces.js";
|
|
6
|
+
|
|
7
|
+
test("resolveConsoleSurfaceIdsFromAppConfig resolves all enabled console-owner surfaces", () => {
|
|
8
|
+
const surfaceIds = resolveConsoleSurfaceIdsFromAppConfig({
|
|
9
|
+
surfaceDefinitions: {
|
|
10
|
+
home: { id: "home", enabled: true, requiresWorkspace: false, accessPolicyId: "public" },
|
|
11
|
+
console: { id: "console", enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" },
|
|
12
|
+
opsConsole: { id: "opsConsole", enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" },
|
|
13
|
+
app: { id: "app", enabled: true, requiresWorkspace: true, accessPolicyId: "workspace_member" },
|
|
14
|
+
disabledConsole: {
|
|
15
|
+
id: "disabledConsole",
|
|
16
|
+
enabled: false,
|
|
17
|
+
requiresWorkspace: false,
|
|
18
|
+
accessPolicyId: "console_owner"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
assert.deepEqual(surfaceIds, ["console", "opsconsole"]);
|
|
24
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createConsoleBootstrapContributor } from "../src/server/consoleBootstrapContributor.js";
|
|
4
|
+
|
|
5
|
+
test("console bootstrap contributor seeds the initial console owner into the existing bootstrap payload", async () => {
|
|
6
|
+
const ownerSeeds = [];
|
|
7
|
+
const contributor = createConsoleBootstrapContributor({
|
|
8
|
+
consoleService: {
|
|
9
|
+
async ensureInitialConsoleMember(userId) {
|
|
10
|
+
ownerSeeds.push(String(userId || ""));
|
|
11
|
+
return String(userId || "");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
assert.equal(contributor.order, 300);
|
|
17
|
+
const contribution = await contributor.contribute({
|
|
18
|
+
payload: {
|
|
19
|
+
session: {
|
|
20
|
+
authenticated: true,
|
|
21
|
+
userId: "12"
|
|
22
|
+
},
|
|
23
|
+
surfaceAccess: {
|
|
24
|
+
existing: true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.deepEqual(ownerSeeds, ["12"]);
|
|
30
|
+
assert.deepEqual(contribution, {
|
|
31
|
+
surfaceAccess: {
|
|
32
|
+
existing: true,
|
|
33
|
+
consoleowner: true
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("console bootstrap contributor exposes a false consoleowner flag for anonymous bootstrap", async () => {
|
|
39
|
+
const contributor = createConsoleBootstrapContributor({
|
|
40
|
+
consoleService: {
|
|
41
|
+
async ensureInitialConsoleMember() {
|
|
42
|
+
throw new Error("should not be called for anonymous payload");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const contribution = await contributor.contribute({
|
|
48
|
+
payload: {
|
|
49
|
+
session: {
|
|
50
|
+
authenticated: false
|
|
51
|
+
},
|
|
52
|
+
surfaceAccess: {
|
|
53
|
+
existing: true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
assert.deepEqual(contribution, {
|
|
59
|
+
surfaceAccess: {
|
|
60
|
+
existing: true,
|
|
61
|
+
consoleowner: false
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { ConsoleCoreServiceProvider } from "../src/server/ConsoleCoreServiceProvider.js";
|
|
4
|
+
|
|
5
|
+
function createReplyDouble() {
|
|
6
|
+
return {
|
|
7
|
+
statusCode: 200,
|
|
8
|
+
payload: null,
|
|
9
|
+
redirectedTo: "",
|
|
10
|
+
code(value) {
|
|
11
|
+
this.statusCode = value;
|
|
12
|
+
return this;
|
|
13
|
+
},
|
|
14
|
+
send(value) {
|
|
15
|
+
this.payload = value;
|
|
16
|
+
return this;
|
|
17
|
+
},
|
|
18
|
+
redirect(value) {
|
|
19
|
+
this.redirectedTo = String(value || "");
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findRoute(routes, { method, path }) {
|
|
26
|
+
return routes.find((route) => route.method === method && route.path === path) || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function registerRoutes({
|
|
30
|
+
consoleService = {}
|
|
31
|
+
} = {}) {
|
|
32
|
+
const registeredRoutes = [];
|
|
33
|
+
const router = {
|
|
34
|
+
register(method, path, route, handler) {
|
|
35
|
+
registeredRoutes.push({
|
|
36
|
+
...route,
|
|
37
|
+
method,
|
|
38
|
+
path,
|
|
39
|
+
handler
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const bindings = new Map([
|
|
45
|
+
["jskit.http.router", router],
|
|
46
|
+
["actionExecutor", {}]
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
bindings.set("consoleService", consoleService);
|
|
50
|
+
|
|
51
|
+
const app = {
|
|
52
|
+
has(token) {
|
|
53
|
+
return bindings.has(token);
|
|
54
|
+
},
|
|
55
|
+
make(token) {
|
|
56
|
+
if (!bindings.has(token)) {
|
|
57
|
+
throw new Error(`Missing test binding for token: ${String(token)}`);
|
|
58
|
+
}
|
|
59
|
+
return bindings.get(token);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const provider = new ConsoleCoreServiceProvider();
|
|
64
|
+
await provider.boot(app);
|
|
65
|
+
|
|
66
|
+
return registeredRoutes;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createActionRequest({ input = {}, executeAction, file = null }) {
|
|
70
|
+
return {
|
|
71
|
+
input,
|
|
72
|
+
executeAction,
|
|
73
|
+
file,
|
|
74
|
+
user: {
|
|
75
|
+
id: 42
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test("console-core boot mounts console routes", async () => {
|
|
81
|
+
const routes = await registerRoutes();
|
|
82
|
+
|
|
83
|
+
assert.equal(findRoute(routes, { method: "GET", path: "/api/console/settings" })?.path, "/api/console/settings");
|
|
84
|
+
assert.equal(findRoute(routes, { method: "PATCH", path: "/api/console/settings" })?.path, "/api/console/settings");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("console settings route handlers use request.input payloads", async () => {
|
|
88
|
+
const routes = await registerRoutes();
|
|
89
|
+
const calls = [];
|
|
90
|
+
const executeAction = async (payload) => {
|
|
91
|
+
calls.push(payload);
|
|
92
|
+
return {
|
|
93
|
+
settings: {}
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await findRoute(routes, { method: "GET", path: "/api/console/settings" }).handler(
|
|
98
|
+
createActionRequest({ executeAction }),
|
|
99
|
+
createReplyDouble()
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
await findRoute(routes, { method: "PATCH", path: "/api/console/settings" }).handler(
|
|
103
|
+
createActionRequest({
|
|
104
|
+
input: {
|
|
105
|
+
body: {}
|
|
106
|
+
},
|
|
107
|
+
executeAction
|
|
108
|
+
}),
|
|
109
|
+
createReplyDouble()
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
assert.equal(calls[0].actionId, "console.settings.read");
|
|
113
|
+
assert.deepEqual(calls[1], {
|
|
114
|
+
actionId: "console.settings.update",
|
|
115
|
+
input: {
|
|
116
|
+
payload: {}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
|
|
10
|
+
function assertResourceShape(resource, label) {
|
|
11
|
+
assert.ok(resource, `${label} resource must exist.`);
|
|
12
|
+
assert.equal(typeof resource, "object", `${label} resource must be an object.`);
|
|
13
|
+
assert.equal(typeof resource.resource, "string", `${label}.resource must be a string.`);
|
|
14
|
+
|
|
15
|
+
for (const operationName of ["view", "list", "create", "replace", "patch"]) {
|
|
16
|
+
const operation = resource.operations?.[operationName];
|
|
17
|
+
assert.equal(typeof operation, "object", `${label}.operations.${operationName} must exist.`);
|
|
18
|
+
assert.equal(typeof operation.method, "string", `${label}.operations.${operationName}.method must exist.`);
|
|
19
|
+
const resolvedMessages =
|
|
20
|
+
operation?.messages && typeof operation.messages === "object"
|
|
21
|
+
? operation.messages
|
|
22
|
+
: resource?.messages || resource?.operationMessages;
|
|
23
|
+
assert.equal(
|
|
24
|
+
typeof resolvedMessages,
|
|
25
|
+
"object",
|
|
26
|
+
`${label}.operations.${operationName} must resolve messages from operation.messages or resource.messages.`
|
|
27
|
+
);
|
|
28
|
+
assert.equal(
|
|
29
|
+
typeof operation.outputValidator?.schema,
|
|
30
|
+
"object",
|
|
31
|
+
`${label}.operations.${operationName} payload schema is required.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
assert.equal(typeof resource.operations.create.bodyValidator?.schema, "object", `${label}.operations.create.bodyValidator.schema is required.`);
|
|
36
|
+
assert.equal(typeof resource.operations.replace.bodyValidator?.schema, "object", `${label}.operations.replace.bodyValidator.schema is required.`);
|
|
37
|
+
assert.equal(typeof resource.operations.patch.bodyValidator?.schema, "object", `${label}.operations.patch.bodyValidator.schema is required.`);
|
|
38
|
+
|
|
39
|
+
const requiredMetadata = deriveResourceRequiredMetadata(resource);
|
|
40
|
+
assert.ok(Array.isArray(requiredMetadata.create), `${label}.derivedRequired.create must be an array.`);
|
|
41
|
+
assert.ok(Array.isArray(requiredMetadata.replace), `${label}.derivedRequired.replace must be an array.`);
|
|
42
|
+
assert.ok(Array.isArray(requiredMetadata.patch), `${label}.derivedRequired.patch must be an array.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("console settings resources expose canonical validators", () => {
|
|
46
|
+
assertResourceShape(consoleSettingsResource, "consoleSettings");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("console settings operations expose canonical validators", () => {
|
|
50
|
+
for (const operationName of ["view", "list", "create", "replace", "patch"]) {
|
|
51
|
+
const operation = consoleSettingsResource.operations?.[operationName];
|
|
52
|
+
assert.equal(typeof operation?.method, "string", `${operationName}.method must exist.`);
|
|
53
|
+
assert.equal(typeof operation?.outputValidator?.schema, "object", `${operationName}.outputValidator.schema must exist.`);
|
|
54
|
+
if (operation?.bodyValidator) {
|
|
55
|
+
assert.equal(typeof operation.bodyValidator.schema, "object", `${operationName}.bodyValidator.schema must exist.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("console-core no longer uses a legacy workspace schema helper path", () => {
|
|
61
|
+
const testFilePath = fileURLToPath(import.meta.url);
|
|
62
|
+
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
63
|
+
const legacyWorkspaceRoutesFile = path.join(packageRoot, "src", "server", "common", "routes", "workspaceRoutes.js");
|
|
64
|
+
assert.equal(existsSync(legacyWorkspaceRoutesFile), false, "workspaceRoutes.js must not exist.");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("console-core route validators do not live under a legacy shared/schema directory", () => {
|
|
68
|
+
const testFilePath = fileURLToPath(import.meta.url);
|
|
69
|
+
const packageRoot = path.resolve(path.dirname(testFilePath), "..");
|
|
70
|
+
const legacySchemaDir = path.join(packageRoot, "src", "shared", "schema");
|
|
71
|
+
assert.equal(existsSync(legacySchemaDir), false, "src/shared/schema must not exist.");
|
|
72
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createService } from "../src/server/consoleSettings/consoleService.js";
|
|
4
|
+
|
|
5
|
+
function createFixture(initialOwnerUserId = null) {
|
|
6
|
+
const state = {
|
|
7
|
+
ownerUserId: initialOwnerUserId
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const service = createService({
|
|
11
|
+
consoleSettingsRepository: {
|
|
12
|
+
async ensureOwnerUserId(userId) {
|
|
13
|
+
const normalizedUserId = String(userId || "");
|
|
14
|
+
if (!state.ownerUserId) {
|
|
15
|
+
state.ownerUserId = normalizedUserId;
|
|
16
|
+
}
|
|
17
|
+
return state.ownerUserId;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return { service, state };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("consoleService seeds the first authenticated user as console owner", async () => {
|
|
26
|
+
const { service, state } = createFixture();
|
|
27
|
+
|
|
28
|
+
const firstOwner = await service.ensureInitialConsoleMember("7");
|
|
29
|
+
const secondAttempt = await service.ensureInitialConsoleMember("9");
|
|
30
|
+
|
|
31
|
+
assert.equal(firstOwner, "7");
|
|
32
|
+
assert.equal(secondAttempt, "7");
|
|
33
|
+
assert.equal(state.ownerUserId, "7");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("consoleService.requireConsoleOwner denies authenticated non-owners", async () => {
|
|
37
|
+
const { service } = createFixture("7");
|
|
38
|
+
|
|
39
|
+
await assert.rejects(
|
|
40
|
+
() =>
|
|
41
|
+
service.requireConsoleOwner({
|
|
42
|
+
actor: {
|
|
43
|
+
id: "9"
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
(error) => error?.status === 403
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("consoleService.requireConsoleOwner requires authentication", async () => {
|
|
51
|
+
const { service } = createFixture("7");
|
|
52
|
+
|
|
53
|
+
await assert.rejects(
|
|
54
|
+
() => service.requireConsoleOwner({}),
|
|
55
|
+
(error) => error?.status === 401
|
|
56
|
+
);
|
|
57
|
+
});
|