openxiangda-cli 2.0.0-alpha.61 → 2.0.0-alpha.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +1 -0
- package/dist/create-workspace.js.map +1 -1
- package/package.json +4 -4
- package/template/AGENTS.md +1 -1
- package/template/README.md +7 -7
- package/template/apps/server/package.json +1 -1
- package/template/apps/server/src/app.module.ts +1 -3
- package/template/apps/server/test/smoke.test.ts +6 -23
- package/template/apps/web/e2e/resources.spec.ts +20 -0
- package/template/apps/web/index.html +1 -1
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +38 -29
- package/template/apps/web/src/Shell.tsx +39 -211
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +1 -1
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +26 -5
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +32 -1
- package/template/apps/web/src/data-provider.ts +23 -102
- package/template/apps/web/src/main.tsx +22 -82
- package/template/apps/web/src/platform-client.ts +71 -415
- package/template/apps/web/src/runtime-meta.ts +1 -1
- package/template/apps/web/src/styles.css +3 -3
- package/template/apps/web/test/contracts.test.ts +31 -769
- package/template/openxiangda.config.ts +14 -81
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +7 -804
- package/template/apps/server/src/instrument-context.controller.ts +0 -25
- package/template/apps/web/e2e/instruments.spec.ts +0 -1338
- package/template/apps/web/src/CollegePage.tsx +0 -181
- package/template/apps/web/src/InstrumentDetailPage.tsx +0 -176
- package/template/apps/web/src/InstrumentForm.tsx +0 -380
- package/template/apps/web/src/InstrumentFormPage.tsx +0 -82
- package/template/apps/web/src/InstrumentListPage.tsx +0 -457
- package/template/apps/web/src/fields.ts +0 -18
- package/template/apps/web/src/instrument.ts +0 -56
- package/template/platform/data/colleges.ts +0 -21
- package/template/platform/data/instrument-surface.ts +0 -125
- package/template/platform/data/instruments.ts +0 -83
|
@@ -1,773 +1,35 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import test from 'node:test';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
assert.
|
|
35
|
-
instrumentFields
|
|
36
|
-
.filter(field => field.createCapability && field.updateCapability)
|
|
37
|
-
.map(field => field.key),
|
|
38
|
-
[
|
|
39
|
-
'instrumentCode',
|
|
40
|
-
'assetCode',
|
|
41
|
-
'assetValue',
|
|
42
|
-
'collegeId',
|
|
43
|
-
'instrumentAdminIds',
|
|
44
|
-
]
|
|
45
|
-
);
|
|
46
|
-
assert.deepEqual(
|
|
47
|
-
instrumentFields.filter(field => field.system).map(field => field.key),
|
|
48
|
-
['id', 'revision']
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('the instrument list, search, and filters are declared by the resource surface', () => {
|
|
53
|
-
assert.deepEqual(
|
|
54
|
-
Object.entries(instrumentSurface.fields)
|
|
55
|
-
.filter(([, field]) => field.list)
|
|
56
|
-
.map(([key]) => key)
|
|
57
|
-
.sort(),
|
|
58
|
-
[
|
|
59
|
-
'chineseName',
|
|
60
|
-
'collegeId',
|
|
61
|
-
'image',
|
|
62
|
-
'instrumentAdminIds',
|
|
63
|
-
'instrumentCode',
|
|
64
|
-
'instrumentStatus',
|
|
65
|
-
'openToOutside',
|
|
66
|
-
'specModel',
|
|
67
|
-
'assetCode',
|
|
68
|
-
].sort()
|
|
69
|
-
);
|
|
70
|
-
assert.deepEqual(instrumentSurface.list?.searchableFields, [
|
|
71
|
-
'chineseName',
|
|
72
|
-
'instrumentCode',
|
|
73
|
-
'assetCode',
|
|
74
|
-
]);
|
|
75
|
-
assert.ok(instrumentSurface.list?.filterFields?.includes('assetValue'));
|
|
76
|
-
const list = readFileSync(
|
|
77
|
-
new URL('../src/InstrumentListPage.tsx', import.meta.url),
|
|
78
|
-
'utf8'
|
|
79
|
-
);
|
|
80
|
-
const client = readFileSync(
|
|
81
|
-
new URL('../src/platform-client.ts', import.meta.url),
|
|
82
|
-
'utf8'
|
|
83
|
-
);
|
|
84
|
-
assert.match(list, /listSurfaceFields/);
|
|
85
|
-
assert.match(list, /searchableFields/);
|
|
86
|
-
assert.match(client, /query\.searchField/);
|
|
4
|
+
import { resourceDefinitions, resourceCodes } from '../../../packages/contracts/src/generated.js';
|
|
5
|
+
import { readRuntimeMount, resolveApplicationBasename } from '../src/runtime-meta';
|
|
6
|
+
|
|
7
|
+
test('clean template is resource-driven and contains no HGY fixture', () => {
|
|
8
|
+
const config = readFileSync(new URL('../../../openxiangda.config.ts', import.meta.url), 'utf8');
|
|
9
|
+
const source = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8');
|
|
10
|
+
assert.deepEqual(Object.keys(resourceDefinitions).sort(), [...resourceCodes].sort());
|
|
11
|
+
assert.match(config, /data:\s*\{\s*resources:/);
|
|
12
|
+
assert.match(source, /Object\.keys\(resourceDefinitions\)/);
|
|
13
|
+
const legacyMarkers = ['instruments', 'colleges', '仪器' + '资源管理', '学院' + '字典', 'HGY', 'Instrument'];
|
|
14
|
+
assert.doesNotMatch(`${config}\n${source}`, new RegExp(legacyMarkers.join('|')));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('standard surfaces keep authoritative directory/resource selectors and mobile routes', () => {
|
|
18
|
+
const surface = readFileSync(new URL('../src/components/resource/SurfaceFields.tsx', import.meta.url), 'utf8');
|
|
19
|
+
const selector = readFileSync(new URL('../src/AuthoritativeSelector.tsx', import.meta.url), 'utf8');
|
|
20
|
+
const crud = readFileSync(new URL('../src/components/resource/GeneratedResourceCrud.tsx', import.meta.url), 'utf8');
|
|
21
|
+
assert.match(surface, /PlatformDirectoryPicker/);
|
|
22
|
+
assert.match(surface, /source="resource"/);
|
|
23
|
+
assert.match(selector, /searchResource|resolveResource/);
|
|
24
|
+
assert.match(crud, /MobileResourceListPage/);
|
|
25
|
+
assert.match(crud, /fieldWritable/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('runtime metadata stays mount-scoped', () => {
|
|
29
|
+
const mount = readRuntimeMount(name => ({
|
|
30
|
+
'openxiangda-runtime-base': '/view/example-app/',
|
|
31
|
+
'openxiangda-app-code': 'example-app',
|
|
32
|
+
'openxiangda-environment': 'preproduction',
|
|
33
|
+
}[name]));
|
|
34
|
+
assert.equal(resolveApplicationBasename(mount), '/view/example-app');
|
|
87
35
|
});
|
|
88
|
-
|
|
89
|
-
test('application shell and instrument surfaces match the approved navigation model', () => {
|
|
90
|
-
const shell = readFileSync(new URL('../src/Shell.tsx', import.meta.url), 'utf8');
|
|
91
|
-
const list = readFileSync(
|
|
92
|
-
new URL('../src/InstrumentListPage.tsx', import.meta.url),
|
|
93
|
-
'utf8'
|
|
94
|
-
);
|
|
95
|
-
const routes = readFileSync(new URL('../src/main.tsx', import.meta.url), 'utf8');
|
|
96
|
-
const form = readFileSync(
|
|
97
|
-
new URL('../src/InstrumentForm.tsx', import.meta.url),
|
|
98
|
-
'utf8'
|
|
99
|
-
);
|
|
100
|
-
const formPage = readFileSync(
|
|
101
|
-
new URL('../src/InstrumentFormPage.tsx', import.meta.url),
|
|
102
|
-
'utf8'
|
|
103
|
-
);
|
|
104
|
-
const colleges = readFileSync(
|
|
105
|
-
new URL('../src/CollegePage.tsx', import.meta.url),
|
|
106
|
-
'utf8'
|
|
107
|
-
);
|
|
108
|
-
const directory = readFileSync(
|
|
109
|
-
new URL('../src/components/platform-fields/PlatformDirectoryPicker.tsx', import.meta.url),
|
|
110
|
-
'utf8'
|
|
111
|
-
);
|
|
112
|
-
const attachment = readFileSync(
|
|
113
|
-
new URL('../src/components/platform-fields/AttachmentFileList.tsx', import.meta.url),
|
|
114
|
-
'utf8'
|
|
115
|
-
);
|
|
116
|
-
const surfaceFields = readFileSync(
|
|
117
|
-
new URL('../src/components/resource/SurfaceFields.tsx', import.meta.url),
|
|
118
|
-
'utf8'
|
|
119
|
-
);
|
|
120
|
-
const preview = readFileSync(
|
|
121
|
-
new URL('../src/FilePreviewPage.tsx', import.meta.url),
|
|
122
|
-
'utf8'
|
|
123
|
-
);
|
|
124
|
-
const standardPages = readFileSync(
|
|
125
|
-
new URL('../src/components/resource/StandardResourcePages.tsx', import.meta.url),
|
|
126
|
-
'utf8'
|
|
127
|
-
);
|
|
128
|
-
const generatedCrud = readFileSync(
|
|
129
|
-
new URL('../src/components/resource/GeneratedResourceCrud.tsx', import.meta.url),
|
|
130
|
-
'utf8'
|
|
131
|
-
);
|
|
132
|
-
for (const marker of [
|
|
133
|
-
'仪器资源管理',
|
|
134
|
-
'工作台',
|
|
135
|
-
'仪器管理',
|
|
136
|
-
'仪器资源',
|
|
137
|
-
'开放记录',
|
|
138
|
-
'基础数据',
|
|
139
|
-
'学院字典',
|
|
140
|
-
'系统设置',
|
|
141
|
-
'应用管理员',
|
|
142
|
-
'收起侧边栏',
|
|
143
|
-
]) {
|
|
144
|
-
assert.match(shell, new RegExp(marker));
|
|
145
|
-
}
|
|
146
|
-
assert.doesNotMatch(shell, /<span>\{identity\.userId\}<\/span>/);
|
|
147
|
-
assert.doesNotMatch(shell, /验证本地 Nest|roleCodes\.join|environment\.key/);
|
|
148
|
-
assert.doesNotMatch(list, /<Drawer|surface="drawer"/);
|
|
149
|
-
assert.match(routes, /path="\/instruments\/new"[\s\S]*<InstrumentFormPage mode="create"/);
|
|
150
|
-
assert.match(routes, /path="\/m\/instruments\/new"[\s\S]*variant="mobile"/);
|
|
151
|
-
assert.match(routes, /path="\/m\/instruments"[\s\S]*variant="mobile"/);
|
|
152
|
-
assert.match(routes, /path="\/m\/instruments\/:id"[\s\S]*variant="mobile"/);
|
|
153
|
-
assert.match(routes, /path="\/m\/instruments\/:id\/edit"[\s\S]*variant="mobile"/);
|
|
154
|
-
assert.match(colleges, /<Drawer/);
|
|
155
|
-
assert.match(colleges, /size=\{520\}/);
|
|
156
|
-
assert.match(directory, /width=\{kind === 'user' \? 980 : 760\}/);
|
|
157
|
-
assert.match(directory, /browseDepartmentTree|browseDepartmentUsers/);
|
|
158
|
-
assert.match(directory, /选择成员|选择部门|组织架构|已选/);
|
|
159
|
-
assert.match(form, /oxa-form-full/);
|
|
160
|
-
assert.match(form, /SurfaceFieldControl/);
|
|
161
|
-
assert.match(surfaceFields, /PlatformDirectoryPicker/);
|
|
162
|
-
assert.match(surfaceFields, /oxa-file-dropzone/);
|
|
163
|
-
assert.match(form, /oxa-detail-layout/);
|
|
164
|
-
assert.match(list, /<ResourceListPage/);
|
|
165
|
-
assert.match(formPage, /ResourceFormPage/);
|
|
166
|
-
assert.match(formPage, /MobileResourceFormPage/);
|
|
167
|
-
assert.match(list, /MobileResourceListPage/);
|
|
168
|
-
assert.match(surfaceFields, /SurfaceFilterControl/);
|
|
169
|
-
assert.match(form, /MobileSurfaceFieldControl/);
|
|
170
|
-
assert.match(
|
|
171
|
-
readFileSync(new URL('../src/InstrumentDetailPage.tsx', import.meta.url), 'utf8'),
|
|
172
|
-
/<ResourceDetailPage/
|
|
173
|
-
);
|
|
174
|
-
assert.match(standardPages, /批量事务合同落地后启用/);
|
|
175
|
-
assert.match(standardPages, /DataResourceSurface/);
|
|
176
|
-
assert.match(generatedCrud, /resourceDefinitions/);
|
|
177
|
-
assert.match(generatedCrud, /createNativeResourceClient/);
|
|
178
|
-
assert.match(generatedCrud, /MobileResourceDetailPage/);
|
|
179
|
-
assert.match(routes, /path="\/files\/:resourceCode\/:fileId\/preview"/);
|
|
180
|
-
assert.match(attachment, /Image\.PreviewGroup/);
|
|
181
|
-
assert.match(attachment, /attachmentPreviewPath/);
|
|
182
|
-
assert.match(preview, /import\('docx-preview'\)/);
|
|
183
|
-
assert.match(preview, /import\('xlsx'\)/);
|
|
184
|
-
assert.match(preview, /只读预览/);
|
|
185
|
-
assert.doesNotMatch(form, /instrumentFileContentUrl|window\.open/);
|
|
186
|
-
assert.doesNotMatch(
|
|
187
|
-
`${form}\n${directory}`,
|
|
188
|
-
/FormContext|tools\/openxiangda|RoleSession/
|
|
189
|
-
);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('college, user, and department selectors have authoritative owners only', () => {
|
|
193
|
-
const appConfig = readFileSync(
|
|
194
|
-
new URL('../../../openxiangda.config.ts', import.meta.url),
|
|
195
|
-
'utf8'
|
|
196
|
-
);
|
|
197
|
-
const colleges = readFileSync(
|
|
198
|
-
new URL('../../../platform/data/colleges.ts', import.meta.url),
|
|
199
|
-
'utf8'
|
|
200
|
-
);
|
|
201
|
-
const fields = readFileSync(
|
|
202
|
-
new URL('../../../platform/data/instrument-surface.ts', import.meta.url),
|
|
203
|
-
'utf8'
|
|
204
|
-
);
|
|
205
|
-
assert.match(appConfig, /valueType:\s*'uuid'/);
|
|
206
|
-
assert.match(appConfig, /kind:\s*'native_resource'/);
|
|
207
|
-
assert.match(appConfig, /resourceCode:\s*'colleges'/);
|
|
208
|
-
assert.match(appConfig, /data:\s*\{\s*resources:\s*\[colleges, instruments\]/);
|
|
209
|
-
assert.match(colleges, /code:\s*'name'.*type:\s*'string'/s);
|
|
210
|
-
assert.match(colleges, /code:\s*'enabled'.*type:\s*'boolean'/s);
|
|
211
|
-
assert.match(fields, /kind:\s*'scope'/);
|
|
212
|
-
assert.match(fields, /kind:\s*'directory-user'/);
|
|
213
|
-
assert.match(fields, /kind:\s*'directory-department'/);
|
|
214
|
-
for (const marker of [
|
|
215
|
-
['college', 'am'].join('-'),
|
|
216
|
-
['user', 'wang'].join('-'),
|
|
217
|
-
['dept', 'am-test'].join('-'),
|
|
218
|
-
]) {
|
|
219
|
-
assert.equal(appConfig.includes(marker), false);
|
|
220
|
-
assert.equal(fields.includes(marker), false);
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('college create restores five fields while update omits college reassignment', () => {
|
|
225
|
-
const protectedFields = instrumentFields.filter(
|
|
226
|
-
field => field.createCapability && field.updateCapability
|
|
227
|
-
);
|
|
228
|
-
const collegeCapabilities = new Set<string>([
|
|
229
|
-
...protectedFields.map(field => field.createCapability!),
|
|
230
|
-
...protectedFields
|
|
231
|
-
.filter(field => field.key !== 'collegeId')
|
|
232
|
-
.map(field => field.updateCapability!),
|
|
233
|
-
]);
|
|
234
|
-
const hasCapability = (capability: string) =>
|
|
235
|
-
collegeCapabilities.has(capability);
|
|
236
|
-
const values = {
|
|
237
|
-
instrumentCode: 'INS-001',
|
|
238
|
-
chineseName: '电子显微镜',
|
|
239
|
-
assetCode: 'ASSET-001',
|
|
240
|
-
assetValue: 100,
|
|
241
|
-
collegeId,
|
|
242
|
-
instrumentAdminIds: [userId],
|
|
243
|
-
revision: 7,
|
|
244
|
-
};
|
|
245
|
-
assert.equal(
|
|
246
|
-
instrumentFieldAllowed(
|
|
247
|
-
instrumentFields.find(field => field.key === 'collegeId')!,
|
|
248
|
-
'create',
|
|
249
|
-
hasCapability
|
|
250
|
-
),
|
|
251
|
-
true
|
|
252
|
-
);
|
|
253
|
-
assert.equal(
|
|
254
|
-
instrumentFieldAllowed(
|
|
255
|
-
instrumentFields.find(field => field.key === 'collegeId')!,
|
|
256
|
-
'edit',
|
|
257
|
-
hasCapability
|
|
258
|
-
),
|
|
259
|
-
false
|
|
260
|
-
);
|
|
261
|
-
assert.deepEqual(sanitizeInstrumentValues(values, 'create', hasCapability), {
|
|
262
|
-
instrumentCode: 'INS-001',
|
|
263
|
-
chineseName: '电子显微镜',
|
|
264
|
-
assetCode: 'ASSET-001',
|
|
265
|
-
assetValue: 100,
|
|
266
|
-
collegeId,
|
|
267
|
-
instrumentAdminIds: [userId],
|
|
268
|
-
});
|
|
269
|
-
assert.deepEqual(sanitizeInstrumentValues(values, 'edit', hasCapability), {
|
|
270
|
-
instrumentCode: 'INS-001',
|
|
271
|
-
chineseName: '电子显微镜',
|
|
272
|
-
assetCode: 'ASSET-001',
|
|
273
|
-
assetValue: 100,
|
|
274
|
-
instrumentAdminIds: [userId],
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test('instrument admin update payload omits every protected field', () => {
|
|
279
|
-
const values = Object.fromEntries(
|
|
280
|
-
instrumentFields
|
|
281
|
-
.filter(field => !field.system)
|
|
282
|
-
.map(field => [field.key, `value:${field.key}`])
|
|
283
|
-
);
|
|
284
|
-
const sanitized = sanitizeInstrumentValues(values, 'edit', () => false);
|
|
285
|
-
assert.equal(sanitized.chineseName, 'value:chineseName');
|
|
286
|
-
for (const field of instrumentFields.filter(
|
|
287
|
-
item => item.createCapability && item.updateCapability
|
|
288
|
-
)) {
|
|
289
|
-
assert.equal(
|
|
290
|
-
Object.prototype.hasOwnProperty.call(sanitized, field.key),
|
|
291
|
-
false
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test('school admin keeps every protected field in create and update payloads', () => {
|
|
297
|
-
const values = Object.fromEntries(
|
|
298
|
-
instrumentFields
|
|
299
|
-
.filter(field => !field.system)
|
|
300
|
-
.map(field => [field.key, `value:${field.key}`])
|
|
301
|
-
);
|
|
302
|
-
for (const mode of ['create', 'edit'] as const) {
|
|
303
|
-
const sanitized = sanitizeInstrumentValues(values, mode, () => true);
|
|
304
|
-
for (const field of instrumentFields.filter(
|
|
305
|
-
item => item.createCapability && item.updateCapability
|
|
306
|
-
)) {
|
|
307
|
-
assert.equal(sanitized[field.key], `value:${field.key}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
test('deployed app/environment come only from platform-injected runtime meta', () => {
|
|
313
|
-
const values = new Map([
|
|
314
|
-
['openxiangda-runtime-base', '/view/instrument-center/'],
|
|
315
|
-
['openxiangda-app-code', 'instrument-center'],
|
|
316
|
-
['openxiangda-environment', 'production'],
|
|
317
|
-
]);
|
|
318
|
-
const mount = readRuntimeMount(name => values.get(name));
|
|
319
|
-
assert.deepEqual(mount, {
|
|
320
|
-
runtimeBase:
|
|
321
|
-
'/view/instrument-center/',
|
|
322
|
-
appCode: 'instrument-center',
|
|
323
|
-
environmentKey: 'production',
|
|
324
|
-
});
|
|
325
|
-
assert.equal(readRuntimeMount(() => undefined), null);
|
|
326
|
-
assert.throws(
|
|
327
|
-
() =>
|
|
328
|
-
readRuntimeMount(name =>
|
|
329
|
-
name === 'openxiangda-environment' ? 'production' : undefined
|
|
330
|
-
),
|
|
331
|
-
/OPENXIANGDA_RUNTIME_META_INVALID/
|
|
332
|
-
);
|
|
333
|
-
assert.equal(
|
|
334
|
-
applicationApiPath('api/instruments/context'),
|
|
335
|
-
'/api/instruments/context'
|
|
336
|
-
);
|
|
337
|
-
assert.equal(resolveApplicationBasename(mount), '/view/instrument-center');
|
|
338
|
-
assert.equal(
|
|
339
|
-
resolveApplicationBasename({
|
|
340
|
-
...mount!,
|
|
341
|
-
runtimeBase: '/dev/instrument-center/',
|
|
342
|
-
environmentKey: 'preproduction',
|
|
343
|
-
}),
|
|
344
|
-
'/dev/instrument-center'
|
|
345
|
-
);
|
|
346
|
-
assert.equal(
|
|
347
|
-
resolveApplicationApiPath(mount, 'api/instruments/context'),
|
|
348
|
-
'/service/openxiangda-app-api/v2/instrument-center/production/api/instruments/context'
|
|
349
|
-
);
|
|
350
|
-
assert.equal(
|
|
351
|
-
resolveAttachmentPreviewPath(
|
|
352
|
-
mount,
|
|
353
|
-
'instruments',
|
|
354
|
-
'77777777-7777-4777-8777-777777777777'
|
|
355
|
-
),
|
|
356
|
-
'/view/instrument-center/files/instruments/77777777-7777-4777-8777-777777777777/preview'
|
|
357
|
-
);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test('managed file preview reads metadata and bytes without a RoleSession', async () => {
|
|
361
|
-
const calls: Array<{ path: string; init?: RequestInit }> = [];
|
|
362
|
-
const originalFetch = globalThis.fetch;
|
|
363
|
-
globalThis.fetch = async (input, init) => {
|
|
364
|
-
const path = String(input);
|
|
365
|
-
calls.push({ path, init });
|
|
366
|
-
if (path.endsWith('/dev-sessions/current')) {
|
|
367
|
-
return Response.json({
|
|
368
|
-
code: 200,
|
|
369
|
-
data: {
|
|
370
|
-
environment: { id: 'test-id', key: 'preproduction' },
|
|
371
|
-
principal: {
|
|
372
|
-
userId,
|
|
373
|
-
roleCodes: ['school_admin'],
|
|
374
|
-
capabilityCodes: [],
|
|
375
|
-
isAppSuperAdmin: false,
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
if (path.includes('/files/file-1/preview?')) {
|
|
381
|
-
return Response.json({
|
|
382
|
-
code: 200,
|
|
383
|
-
data: {
|
|
384
|
-
schemaVersion: 'openxiangda.data-file-preview/v2',
|
|
385
|
-
resourceCode: 'instruments',
|
|
386
|
-
file: {
|
|
387
|
-
schemaVersion: 'openxiangda.data-file-ref/v2',
|
|
388
|
-
id: 'file-1',
|
|
389
|
-
name: 'manual.docx',
|
|
390
|
-
size: 12,
|
|
391
|
-
contentType:
|
|
392
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
393
|
-
},
|
|
394
|
-
extension: 'docx',
|
|
395
|
-
previewType: 'office',
|
|
396
|
-
renderMode: 'docx-html',
|
|
397
|
-
previewSurface: 'document',
|
|
398
|
-
previewProvider: 'browser',
|
|
399
|
-
canPreview: true,
|
|
400
|
-
canDownload: true,
|
|
401
|
-
},
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
if (path.includes('/files/file-1/content?')) {
|
|
405
|
-
return new Response(new Uint8Array([80, 75, 3, 4]), {
|
|
406
|
-
status: 200,
|
|
407
|
-
headers: {
|
|
408
|
-
'content-type':
|
|
409
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
410
|
-
},
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
return Response.json({ code: 404, message: 'not mocked' }, { status: 404 });
|
|
414
|
-
};
|
|
415
|
-
try {
|
|
416
|
-
await loadCurrentIdentity();
|
|
417
|
-
const preview = await loadDataFilePreview('instruments', 'file-1');
|
|
418
|
-
const blob = await fetchDataFileBlob('instruments', 'file-1');
|
|
419
|
-
assert.equal(preview.renderMode, 'docx-html');
|
|
420
|
-
assert.equal(blob.size, 4);
|
|
421
|
-
} finally {
|
|
422
|
-
globalThis.fetch = originalFetch;
|
|
423
|
-
}
|
|
424
|
-
const previewCall = calls.find(call => call.path.includes('/files/file-1/preview?'))!;
|
|
425
|
-
const contentCall = calls.find(call => call.path.includes('/files/file-1/content?'))!;
|
|
426
|
-
assert.match(previewCall.path, /environmentKey=preproduction/);
|
|
427
|
-
assert.match(contentCall.path, /disposition=inline/);
|
|
428
|
-
assert.equal(
|
|
429
|
-
calls.some(call =>
|
|
430
|
-
Object.keys(call.init?.headers || {}).some(header =>
|
|
431
|
-
header.toLowerCase().includes('role-session')
|
|
432
|
-
)
|
|
433
|
-
),
|
|
434
|
-
false
|
|
435
|
-
);
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
test('list uses the native Data API with server paging, filters, and sorting', async () => {
|
|
439
|
-
const calls: Array<{ path: string; init?: RequestInit }> = [];
|
|
440
|
-
const originalFetch = globalThis.fetch;
|
|
441
|
-
globalThis.fetch = async (input, init) => {
|
|
442
|
-
calls.push({ path: String(input), init });
|
|
443
|
-
if (String(input).endsWith('/dev-sessions/current')) {
|
|
444
|
-
return new Response(
|
|
445
|
-
JSON.stringify({
|
|
446
|
-
code: 200,
|
|
447
|
-
data: {
|
|
448
|
-
environment: { id: 'test-id', key: 'preproduction' },
|
|
449
|
-
principal: {
|
|
450
|
-
userId: 'user-test',
|
|
451
|
-
roleCodes: ['college_admin'],
|
|
452
|
-
capabilityCodes: [],
|
|
453
|
-
isAppSuperAdmin: false,
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
}),
|
|
457
|
-
{ status: 200, headers: { 'content-type': 'application/json' } }
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
return new Response(
|
|
461
|
-
JSON.stringify({
|
|
462
|
-
code: 200,
|
|
463
|
-
data: {
|
|
464
|
-
schemaVersion: 'openxiangda.data-page/v2',
|
|
465
|
-
resourceCode: 'instruments',
|
|
466
|
-
items: [],
|
|
467
|
-
total: 0,
|
|
468
|
-
limit: 20,
|
|
469
|
-
offset: 20,
|
|
470
|
-
},
|
|
471
|
-
}),
|
|
472
|
-
{ status: 200, headers: { 'content-type': 'application/json' } }
|
|
473
|
-
);
|
|
474
|
-
};
|
|
475
|
-
try {
|
|
476
|
-
await loadCurrentIdentity();
|
|
477
|
-
await instrumentDataApi.list({
|
|
478
|
-
page: 2,
|
|
479
|
-
pageSize: 20,
|
|
480
|
-
collegeId,
|
|
481
|
-
instrumentAdminId: userId,
|
|
482
|
-
sort: { field: 'assetValue', order: 'asc' },
|
|
483
|
-
});
|
|
484
|
-
await instrumentDataApi.list({ page: 1, pageSize: 20 });
|
|
485
|
-
} finally {
|
|
486
|
-
globalThis.fetch = originalFetch;
|
|
487
|
-
}
|
|
488
|
-
assert.equal(calls.length, 3);
|
|
489
|
-
assert.match(
|
|
490
|
-
calls[1]!.path,
|
|
491
|
-
/\/native\/data\/instruments\/query$/
|
|
492
|
-
);
|
|
493
|
-
const body = JSON.parse(String(calls[1]!.init?.body));
|
|
494
|
-
const resourceSource = readFileSync(
|
|
495
|
-
new URL('../../../platform/data/instruments.ts', import.meta.url),
|
|
496
|
-
'utf8'
|
|
497
|
-
);
|
|
498
|
-
const declaredFields = new Set(
|
|
499
|
-
[...resourceSource.matchAll(/\{\s*code:\s*'([^']+)'/g)].map(
|
|
500
|
-
match => match[1]!
|
|
501
|
-
)
|
|
502
|
-
);
|
|
503
|
-
assert.deepEqual(
|
|
504
|
-
[...declaredFields],
|
|
505
|
-
instrumentFields.filter(field => !field.system).map(field => field.key)
|
|
506
|
-
);
|
|
507
|
-
assert.equal(body.environmentKey, 'preproduction');
|
|
508
|
-
assert.equal(body.limit, 20);
|
|
509
|
-
assert.equal(body.offset, 20);
|
|
510
|
-
assert.deepEqual(body.order, [{ field: 'assetValue', direction: 'asc' }]);
|
|
511
|
-
assert.equal(
|
|
512
|
-
[...body.filters, ...body.order].every(item =>
|
|
513
|
-
declaredFields.has(item.field)
|
|
514
|
-
),
|
|
515
|
-
true
|
|
516
|
-
);
|
|
517
|
-
assert.deepEqual(body.filters, [
|
|
518
|
-
{ field: 'collegeId', operator: 'eq', value: collegeId },
|
|
519
|
-
{
|
|
520
|
-
field: 'instrumentAdminIds',
|
|
521
|
-
operator: 'cs',
|
|
522
|
-
value: [userId],
|
|
523
|
-
},
|
|
524
|
-
]);
|
|
525
|
-
const defaultBody = JSON.parse(String(calls[2]!.init?.body));
|
|
526
|
-
assert.deepEqual(defaultBody.order, [
|
|
527
|
-
{ field: 'instrumentCode', direction: 'asc' },
|
|
528
|
-
]);
|
|
529
|
-
assert.equal(
|
|
530
|
-
[...defaultBody.filters, ...defaultBody.order].every(item =>
|
|
531
|
-
declaredFields.has(item.field)
|
|
532
|
-
),
|
|
533
|
-
true
|
|
534
|
-
);
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
test('list rejects an undeclared sorter before issuing a Data API request', async () => {
|
|
538
|
-
let requests = 0;
|
|
539
|
-
const originalFetch = globalThis.fetch;
|
|
540
|
-
globalThis.fetch = async () => {
|
|
541
|
-
requests += 1;
|
|
542
|
-
throw new Error('unexpected fetch');
|
|
543
|
-
};
|
|
544
|
-
try {
|
|
545
|
-
await assert.rejects(
|
|
546
|
-
instrumentDataApi.list({
|
|
547
|
-
page: 1,
|
|
548
|
-
pageSize: 20,
|
|
549
|
-
sort: { field: 'updatedAt', order: 'desc' },
|
|
550
|
-
}),
|
|
551
|
-
/OPENXIANGDA_INSTRUMENT_QUERY_FIELD_NOT_DECLARED:updatedAt/
|
|
552
|
-
);
|
|
553
|
-
} finally {
|
|
554
|
-
globalThis.fetch = originalFetch;
|
|
555
|
-
}
|
|
556
|
-
assert.equal(requests, 0);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
test('authoritative selectors page, resolve exact IDs, and never send RoleSession', async () => {
|
|
560
|
-
const calls: Array<{ path: string; init?: RequestInit }> = [];
|
|
561
|
-
const originalFetch = globalThis.fetch;
|
|
562
|
-
globalThis.fetch = async (input, init) => {
|
|
563
|
-
const path = String(input);
|
|
564
|
-
calls.push({ path, init });
|
|
565
|
-
if (path.endsWith('/dev-sessions/current')) {
|
|
566
|
-
return Response.json({
|
|
567
|
-
code: 200,
|
|
568
|
-
data: {
|
|
569
|
-
environment: {
|
|
570
|
-
id: 'test-id',
|
|
571
|
-
key: 'preproduction',
|
|
572
|
-
activeAppVersionId: 'version-1',
|
|
573
|
-
headRevision: 2,
|
|
574
|
-
authzRevisionId: 'authz-1',
|
|
575
|
-
authzVersion: 3,
|
|
576
|
-
scopeDataVersion: 'scope-1',
|
|
577
|
-
},
|
|
578
|
-
principal: {
|
|
579
|
-
userId,
|
|
580
|
-
roleCodes: ['school_admin'],
|
|
581
|
-
capabilityCodes: [],
|
|
582
|
-
isAppSuperAdmin: false,
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
if (path.includes('/directory/users?')) {
|
|
588
|
-
return Response.json({
|
|
589
|
-
code: 200,
|
|
590
|
-
data: {
|
|
591
|
-
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
592
|
-
kind: 'user',
|
|
593
|
-
items: [
|
|
594
|
-
{ kind: 'user', id: userId, label: '王老师', selectable: true },
|
|
595
|
-
],
|
|
596
|
-
nextCursor: 'next-user-page',
|
|
597
|
-
},
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
if (path.includes('/directory/departments/tree?')) {
|
|
601
|
-
return Response.json({
|
|
602
|
-
code: 200,
|
|
603
|
-
data: {
|
|
604
|
-
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
605
|
-
kind: 'department',
|
|
606
|
-
items: [
|
|
607
|
-
{
|
|
608
|
-
kind: 'department',
|
|
609
|
-
id: 'department-root',
|
|
610
|
-
label: '材料学院',
|
|
611
|
-
hasChildren: true,
|
|
612
|
-
selectable: true,
|
|
613
|
-
},
|
|
614
|
-
],
|
|
615
|
-
nextCursor: null,
|
|
616
|
-
},
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
if (path.includes('/directory/departments/department-root/users?')) {
|
|
620
|
-
return Response.json({
|
|
621
|
-
code: 200,
|
|
622
|
-
data: {
|
|
623
|
-
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
624
|
-
kind: 'user',
|
|
625
|
-
items: [
|
|
626
|
-
{ kind: 'user', id: userId, label: '王老师', selectable: true },
|
|
627
|
-
],
|
|
628
|
-
nextCursor: null,
|
|
629
|
-
},
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
if (path.includes('/directory/resolve?')) {
|
|
633
|
-
const body = JSON.parse(String(init?.body));
|
|
634
|
-
return Response.json({
|
|
635
|
-
code: 200,
|
|
636
|
-
data: {
|
|
637
|
-
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
638
|
-
kind: body.kind,
|
|
639
|
-
items: body.ids.map((id: string) => ({
|
|
640
|
-
kind: body.kind,
|
|
641
|
-
id,
|
|
642
|
-
label: '精确解析',
|
|
643
|
-
selectable: false,
|
|
644
|
-
})),
|
|
645
|
-
nextCursor: null,
|
|
646
|
-
},
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
if (path.includes('/scope-values/resolve?')) {
|
|
650
|
-
const body = JSON.parse(String(init?.body));
|
|
651
|
-
return Response.json({
|
|
652
|
-
code: 200,
|
|
653
|
-
data: scopePage(
|
|
654
|
-
body.operation,
|
|
655
|
-
body.values.map((value: string) => ({
|
|
656
|
-
value,
|
|
657
|
-
label: '先进材料学院',
|
|
658
|
-
selectable: false,
|
|
659
|
-
}))
|
|
660
|
-
),
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
if (path.includes('/scope-values?')) {
|
|
664
|
-
return Response.json({
|
|
665
|
-
code: 200,
|
|
666
|
-
data: scopePage('create', [
|
|
667
|
-
{ value: collegeId, label: '先进材料学院', selectable: true },
|
|
668
|
-
]),
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
return Response.json({ code: 404, message: 'not mocked' }, { status: 404 });
|
|
672
|
-
};
|
|
673
|
-
try {
|
|
674
|
-
await loadCurrentIdentity();
|
|
675
|
-
const users = await searchDirectory('user', {
|
|
676
|
-
keyword: '王老',
|
|
677
|
-
cursor: 'cursor-1',
|
|
678
|
-
});
|
|
679
|
-
assert.equal(users.nextCursor, 'next-user-page');
|
|
680
|
-
assert.equal((await resolveDirectory('department', [userId])).items[0]?.selectable, false);
|
|
681
|
-
assert.equal(
|
|
682
|
-
(await browseDepartmentTree({ parentId: 'department-parent' })).items[0]
|
|
683
|
-
?.id,
|
|
684
|
-
'department-root'
|
|
685
|
-
);
|
|
686
|
-
assert.equal(
|
|
687
|
-
(await browseDepartmentUsers('department-root', 2)).items[0]?.id,
|
|
688
|
-
userId
|
|
689
|
-
);
|
|
690
|
-
assert.equal(
|
|
691
|
-
(await searchCollegeScope('create', { keyword: '', cursor: 'scope-1' }))
|
|
692
|
-
.items[0]?.value,
|
|
693
|
-
collegeId
|
|
694
|
-
);
|
|
695
|
-
assert.equal(
|
|
696
|
-
(await resolveCollegeScope('update', [collegeId])).items[0]?.selectable,
|
|
697
|
-
false
|
|
698
|
-
);
|
|
699
|
-
await assert.rejects(
|
|
700
|
-
resolveDirectory('user', []),
|
|
701
|
-
/OPENXIANGDA_DIRECTORY_RESOLVE_IDS_REQUIRED/
|
|
702
|
-
);
|
|
703
|
-
await assert.rejects(
|
|
704
|
-
resolveCollegeScope('update', []),
|
|
705
|
-
/OPENXIANGDA_SCOPE_RESOLVE_VALUES_REQUIRED/
|
|
706
|
-
);
|
|
707
|
-
await assert.rejects(
|
|
708
|
-
resolveDirectory(
|
|
709
|
-
'user',
|
|
710
|
-
Array.from({ length: 51 }, (_, index) => `user-${index}`)
|
|
711
|
-
),
|
|
712
|
-
/OPENXIANGDA_DIRECTORY_RESOLVE_IDS_EXCEEDED/
|
|
713
|
-
);
|
|
714
|
-
await assert.rejects(
|
|
715
|
-
resolveCollegeScope(
|
|
716
|
-
'update',
|
|
717
|
-
Array.from({ length: 51 }, (_, index) =>
|
|
718
|
-
`${String(index).padStart(8, '0')}-0000-4000-8000-000000000000`
|
|
719
|
-
)
|
|
720
|
-
),
|
|
721
|
-
/OPENXIANGDA_SCOPE_RESOLVE_VALUES_EXCEEDED/
|
|
722
|
-
);
|
|
723
|
-
} finally {
|
|
724
|
-
globalThis.fetch = originalFetch;
|
|
725
|
-
}
|
|
726
|
-
const userSearch = calls.find(call => call.path.includes('/directory/users?'))!;
|
|
727
|
-
const treeBrowse = calls.find(call => call.path.includes('/directory/departments/tree?'))!;
|
|
728
|
-
const memberBrowse = calls.find(call => call.path.includes('/directory/departments/department-root/users?'))!;
|
|
729
|
-
const scopeSearch = calls.find(call => call.path.includes('/scope-values?'))!;
|
|
730
|
-
assert.match(userSearch.path, /keyword=%E7%8E%8B%E8%80%81/);
|
|
731
|
-
assert.match(userSearch.path, /cursor=cursor-1/);
|
|
732
|
-
assert.match(treeBrowse.path, /parentId=department-parent/);
|
|
733
|
-
assert.match(memberBrowse.path, /page=2/);
|
|
734
|
-
assert.match(scopeSearch.path, /environmentKey=preproduction/);
|
|
735
|
-
assert.match(
|
|
736
|
-
scopeSearch.path,
|
|
737
|
-
/\/native\/data-resources\/instruments\/fields\/collegeId\/scope-values\?/
|
|
738
|
-
);
|
|
739
|
-
assert.match(scopeSearch.path, /operation=create/);
|
|
740
|
-
assert.match(scopeSearch.path, /cursor=scope-1/);
|
|
741
|
-
assert.equal(
|
|
742
|
-
calls.some(call =>
|
|
743
|
-
Object.keys(call.init?.headers || {}).some(header =>
|
|
744
|
-
header.toLowerCase().includes('role-session')
|
|
745
|
-
)
|
|
746
|
-
),
|
|
747
|
-
false
|
|
748
|
-
);
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
function scopePage(
|
|
752
|
-
operation: 'create' | 'update',
|
|
753
|
-
items: Array<{ value: string; label: string; selectable: boolean }>
|
|
754
|
-
) {
|
|
755
|
-
return {
|
|
756
|
-
schemaVersion: 'openxiangda.native-scope-value-page/v2',
|
|
757
|
-
environment: {
|
|
758
|
-
id: 'test-id',
|
|
759
|
-
key: 'preproduction',
|
|
760
|
-
activeAppVersionId: 'version-1',
|
|
761
|
-
headRevision: 2,
|
|
762
|
-
authzRevisionId: 'authz-1',
|
|
763
|
-
authzVersion: 3,
|
|
764
|
-
scopeDataVersion: 'scope-1',
|
|
765
|
-
},
|
|
766
|
-
resourceCode: 'instruments',
|
|
767
|
-
fieldCode: 'collegeId',
|
|
768
|
-
dimensionCode: 'college',
|
|
769
|
-
operation,
|
|
770
|
-
items,
|
|
771
|
-
nextCursor: null,
|
|
772
|
-
};
|
|
773
|
-
}
|