whistle.mock-plugins 1.3.1 → 1.4.0-beta.0
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/VERSION.md +15 -0
- package/app/cgi-bin/settings.js +1 -0
- package/app/cgi-bin/webdav.js +56 -0
- package/app/public/js/index.js +1 -1
- package/app/public/js/index.js.LICENSE.txt +12 -0
- package/lib/routes/settings.js +1 -0
- package/lib/webdav-sync.js +806 -0
- package/package.json +1 -1
- package/test/api-error.test.js +29 -0
- package/test/webdav-sync.test.js +428 -0
- package/ui/src/components/WebDavSyncModal.js +516 -0
- package/ui/src/pages/MockDataV2.js +35 -2
- package/ui/src/pages/Settings.js +43 -1
- package/ui/src/styles/webdav-sync.css +408 -0
- package/ui/src/utils/api-error.js +57 -0
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { getApiError } = require('../ui/src/utils/api-error');
|
|
2
|
+
|
|
3
|
+
describe('WebDAV 前端错误提示', () => {
|
|
4
|
+
test('后端返回对象时读取 message 和 conflict', () => {
|
|
5
|
+
expect(getApiError({
|
|
6
|
+
response: {
|
|
7
|
+
status: 409,
|
|
8
|
+
data: { message: '远端数据较新', conflict: 'REMOTE_NEWER' },
|
|
9
|
+
},
|
|
10
|
+
}, '推送失败')).toEqual({
|
|
11
|
+
message: '远端数据较新',
|
|
12
|
+
conflict: 'REMOTE_NEWER',
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('字符串、HTML 和空响应始终生成非空提示', () => {
|
|
17
|
+
expect(getApiError({ response: { status: 502, data: '上游连接失败' } }, '连接失败').message)
|
|
18
|
+
.toBe('上游连接失败');
|
|
19
|
+
expect(getApiError({ response: { status: 500, data: '<html><body>Internal Server Error</body></html>' } }, '连接失败').message)
|
|
20
|
+
.toBe('Internal Server Error');
|
|
21
|
+
expect(getApiError({ response: { status: 504, data: '' } }, '连接失败').message)
|
|
22
|
+
.toContain('HTTP 504');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('纯网络错误使用明确兜底信息', () => {
|
|
26
|
+
expect(getApiError({ message: 'Network Error' }, '连接失败').message)
|
|
27
|
+
.toContain('网络请求失败');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const zlib = require('zlib');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const {
|
|
7
|
+
WEBDAV_FILE_NAME,
|
|
8
|
+
createWebDavSync,
|
|
9
|
+
createModuleFileName,
|
|
10
|
+
getModuleFileUrl,
|
|
11
|
+
getWebDavFileUrl,
|
|
12
|
+
normalizeWebDavEndpoint,
|
|
13
|
+
validateManifest,
|
|
14
|
+
validateModuleData,
|
|
15
|
+
validateSyncData,
|
|
16
|
+
} = require('../lib/webdav-sync');
|
|
17
|
+
const settingsController = require('../app/cgi-bin/settings');
|
|
18
|
+
|
|
19
|
+
const createDataDir = () => fs.mkdtempSync(path.join(os.tmpdir(), 'whistle-mock-webdav-'));
|
|
20
|
+
const jsonBuffer = value => Buffer.from(typeof value == 'string' ? value : JSON.stringify(value));
|
|
21
|
+
const sha256 = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
22
|
+
|
|
23
|
+
const createModule = (id, name, interfaceId = `${id}-interface`) => ({
|
|
24
|
+
feature: { id, name, active: true },
|
|
25
|
+
interfaces: [{ id: interfaceId, featureId: id, name: `${name}接口`, urlPattern: `/api/${id}` }],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const writeLocalData = (dataDir, modules, updatedAt = Date.now()) => {
|
|
29
|
+
const featuresFile = path.join(dataDir, 'features.json');
|
|
30
|
+
const interfacesFile = path.join(dataDir, 'interfaces.json');
|
|
31
|
+
fs.writeJsonSync(featuresFile, { features: modules.map(moduleItem => moduleItem.feature) });
|
|
32
|
+
fs.writeJsonSync(interfacesFile, { interfaces: modules.flatMap(moduleItem => moduleItem.interfaces) });
|
|
33
|
+
const time = new Date(updatedAt);
|
|
34
|
+
fs.utimesSync(featuresFile, time, time);
|
|
35
|
+
fs.utimesSync(interfacesFile, time, time);
|
|
36
|
+
return { featuresFile, interfacesFile };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const writeConfig = (dataDir) => {
|
|
40
|
+
fs.writeJsonSync(path.join(dataDir, 'webdav-config.json'), {
|
|
41
|
+
endpoint: 'https://webdav.example.com/webdav/whistle-mock-plugin-sync/',
|
|
42
|
+
username: 'tester',
|
|
43
|
+
password: 'secret-password',
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const createRemoteModule = (moduleItem, updatedAt = 2000) => {
|
|
48
|
+
const payload = {
|
|
49
|
+
version: 1,
|
|
50
|
+
updatedAt,
|
|
51
|
+
feature: moduleItem.feature,
|
|
52
|
+
interfaces: moduleItem.interfaces,
|
|
53
|
+
};
|
|
54
|
+
const compressed = zlib.gzipSync(jsonBuffer(payload), { level: zlib.constants.Z_BEST_SPEED });
|
|
55
|
+
const contentSha256 = sha256(jsonBuffer({ feature: moduleItem.feature, interfaces: moduleItem.interfaces }));
|
|
56
|
+
const compressedSha256 = sha256(compressed);
|
|
57
|
+
return {
|
|
58
|
+
compressed,
|
|
59
|
+
entry: {
|
|
60
|
+
id: moduleItem.feature.id,
|
|
61
|
+
name: moduleItem.feature.name,
|
|
62
|
+
fileName: createModuleFileName(moduleItem.feature.id, compressedSha256),
|
|
63
|
+
updatedAt,
|
|
64
|
+
interfaceCount: moduleItem.interfaces.length,
|
|
65
|
+
sha256: compressedSha256,
|
|
66
|
+
contentSha256,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const createManifest = (remoteModules, updatedAt = 2000) => ({
|
|
72
|
+
version: 1,
|
|
73
|
+
updatedAt,
|
|
74
|
+
modules: remoteModules.map(moduleItem => moduleItem.entry),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const createResponse = () => ({
|
|
78
|
+
statusCode: 200,
|
|
79
|
+
body: null,
|
|
80
|
+
status(code) {
|
|
81
|
+
this.statusCode = code;
|
|
82
|
+
return this;
|
|
83
|
+
},
|
|
84
|
+
json(body) {
|
|
85
|
+
this.body = body;
|
|
86
|
+
return this;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('WebDAV 模块同步', () => {
|
|
91
|
+
const dataDirs = [];
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
dataDirs.splice(0).forEach(dataDir => fs.removeSync(dataDir));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('规范化目录并生成安全的清单和模块地址', () => {
|
|
98
|
+
expect(normalizeWebDavEndpoint('https://webdav.example.com/webdav/demo?x=1#hash'))
|
|
99
|
+
.toBe('https://webdav.example.com/webdav/demo/');
|
|
100
|
+
expect(getWebDavFileUrl('https://webdav.example.com/webdav/demo'))
|
|
101
|
+
.toBe(`https://webdav.example.com/webdav/demo/${WEBDAV_FILE_NAME}`);
|
|
102
|
+
const fileName = createModuleFileName('../中文/feature', 'a'.repeat(64));
|
|
103
|
+
expect(fileName).toMatch(/^whistle-mock-module-[a-f0-9]{16}-[a-f0-9]{24}\.json\.gz$/);
|
|
104
|
+
expect(getModuleFileUrl('https://webdav.example.com/webdav/demo', fileName))
|
|
105
|
+
.toBe(`https://webdav.example.com/webdav/demo/${fileName}`);
|
|
106
|
+
expect(() => normalizeWebDavEndpoint('ftp://webdav.example.com/demo')).toThrow('只支持 HTTP 或 HTTPS');
|
|
107
|
+
expect(() => getModuleFileUrl('https://webdav.example.com/demo', '../secret.gz')).toThrow('文件名无效');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('拒绝异常的清单、模块和跨模块接口引用', () => {
|
|
111
|
+
expect(() => validateManifest({ version: 1, updatedAt: 1, modules: [{ id: 'x' }] }))
|
|
112
|
+
.toThrow('无效的模块');
|
|
113
|
+
expect(() => validateModuleData({
|
|
114
|
+
version: 1,
|
|
115
|
+
updatedAt: 1,
|
|
116
|
+
feature: { id: 'feature-1', name: '功能' },
|
|
117
|
+
interfaces: [{ id: 'interface-1', featureId: 'feature-2' }],
|
|
118
|
+
})).toThrow('未关联模块');
|
|
119
|
+
expect(() => validateSyncData({
|
|
120
|
+
version: 1,
|
|
121
|
+
updatedAt: 1,
|
|
122
|
+
features: [createModule('feature-1', '功能').feature],
|
|
123
|
+
interfaces: [{ id: 'interface-1', featureId: 'missing' }],
|
|
124
|
+
})).toThrow('未关联有效的功能模块');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('使用 PROPFIND 测试连接,状态展示模块名且不返回密码', async () => {
|
|
128
|
+
const dataDir = createDataDir();
|
|
129
|
+
dataDirs.push(dataDir);
|
|
130
|
+
writeLocalData(dataDir, [createModule('feature-1', '用户中心')]);
|
|
131
|
+
let request;
|
|
132
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
133
|
+
request = config;
|
|
134
|
+
return { status: 207, data: '', headers: {} };
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await service.connect({
|
|
138
|
+
endpoint: 'https://webdav.example.com/webdav/demo',
|
|
139
|
+
username: 'tester',
|
|
140
|
+
password: 'secret-password',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(request.method).toBe('PROPFIND');
|
|
144
|
+
expect(request.headers.Depth).toBe('0');
|
|
145
|
+
expect(service.getStatus()).toEqual(expect.objectContaining({
|
|
146
|
+
configured: true,
|
|
147
|
+
username: 'tester',
|
|
148
|
+
localModules: [expect.objectContaining({ id: 'feature-1', name: '用户中心', interfaceCount: 1 })],
|
|
149
|
+
}));
|
|
150
|
+
expect(service.getStatus()).not.toHaveProperty('password');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('鉴权和网络失败返回明确错误', async () => {
|
|
154
|
+
const dataDir = createDataDir();
|
|
155
|
+
dataDirs.push(dataDir);
|
|
156
|
+
const authService = createWebDavSync(dataDir, async () => ({ status: 401, data: '', headers: {} }));
|
|
157
|
+
await expect(authService.test({ endpoint: 'https://webdav.example.com/demo/', username: 'x', password: 'x' }))
|
|
158
|
+
.rejects.toThrow('用户名或密码错误');
|
|
159
|
+
|
|
160
|
+
const networkService = createWebDavSync(dataDir, async () => {
|
|
161
|
+
const error = new Error('getaddrinfo ENOTFOUND');
|
|
162
|
+
error.code = 'ENOTFOUND';
|
|
163
|
+
throw error;
|
|
164
|
+
});
|
|
165
|
+
await expect(networkService.test({ endpoint: 'https://webdav.invalid/demo/', username: 'x', password: 'x' }))
|
|
166
|
+
.rejects.toThrow('域名解析失败');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('只推送勾选模块,每个模块独立 gzip,最后条件写入清单', async () => {
|
|
170
|
+
const dataDir = createDataDir();
|
|
171
|
+
dataDirs.push(dataDir);
|
|
172
|
+
const userModule = createModule('feature-user', '用户中心');
|
|
173
|
+
const orderModule = createModule('feature-order', '订单中心');
|
|
174
|
+
writeLocalData(dataDir, [userModule, orderModule], 3000);
|
|
175
|
+
writeConfig(dataDir);
|
|
176
|
+
const requests = [];
|
|
177
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
178
|
+
requests.push(config);
|
|
179
|
+
if (config.method == 'GET') return { status: 404, data: '', headers: {} };
|
|
180
|
+
return { status: config.method == 'PUT' ? 201 : 204, data: '', headers: {} };
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const status = await service.push({ featureIds: ['feature-user'] });
|
|
184
|
+
const gzipPut = requests.find(request => request.method == 'PUT' && request.headers['Content-Type'] == 'application/gzip');
|
|
185
|
+
const manifestPut = requests.find(request => request.method == 'PUT' && request.url.endsWith(WEBDAV_FILE_NAME));
|
|
186
|
+
const payload = JSON.parse(zlib.gunzipSync(gzipPut.data).toString('utf8'));
|
|
187
|
+
const manifest = JSON.parse(manifestPut.data.toString('utf8'));
|
|
188
|
+
|
|
189
|
+
expect(payload.feature.name).toBe('用户中心');
|
|
190
|
+
expect(payload.interfaces).toHaveLength(1);
|
|
191
|
+
expect(payload.interfaces[0].featureId).toBe('feature-user');
|
|
192
|
+
expect(manifest.modules).toHaveLength(1);
|
|
193
|
+
expect(manifest.modules[0].name).toBe('用户中心');
|
|
194
|
+
expect(manifestPut.headers['If-None-Match']).toBe('*');
|
|
195
|
+
expect(requests.findIndex(request => request == manifestPut)).toBeGreaterThan(requests.findIndex(request => request == gzipPut));
|
|
196
|
+
expect(status.remoteModules.map(moduleItem => moduleItem.name)).toEqual(['用户中心']);
|
|
197
|
+
expect(gzipPut.data.toString('utf8')).not.toContain('secret-password');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('未传模块 ID 时默认全选,并为每个模块生成一个 gzip', async () => {
|
|
201
|
+
const dataDir = createDataDir();
|
|
202
|
+
dataDirs.push(dataDir);
|
|
203
|
+
writeLocalData(dataDir, [createModule('feature-user', '用户中心'), createModule('feature-order', '订单中心')]);
|
|
204
|
+
writeConfig(dataDir);
|
|
205
|
+
const requests = [];
|
|
206
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
207
|
+
requests.push(config);
|
|
208
|
+
if (config.method == 'GET') return { status: 404, data: '', headers: {} };
|
|
209
|
+
return { status: config.method == 'PUT' ? 201 : 204, data: '', headers: {} };
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await service.push();
|
|
213
|
+
const gzipPuts = requests.filter(request => request.method == 'PUT' && request.headers['Content-Type'] == 'application/gzip');
|
|
214
|
+
const payloadNames = gzipPuts.map(request => JSON.parse(zlib.gunzipSync(request.data).toString('utf8')).feature.name);
|
|
215
|
+
expect(gzipPuts).toHaveLength(2);
|
|
216
|
+
expect(payloadNames).toEqual(['用户中心', '订单中心']);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('局部推送保留未选远端模块并使用 ETag 更新清单', async () => {
|
|
220
|
+
const dataDir = createDataDir();
|
|
221
|
+
dataDirs.push(dataDir);
|
|
222
|
+
const userModule = createModule('feature-user', '用户中心');
|
|
223
|
+
const newOrderModule = createModule('feature-order', '新订单中心');
|
|
224
|
+
const oldOrderModule = createModule('feature-order', '旧订单中心');
|
|
225
|
+
const remoteUser = createRemoteModule(userModule, 1000);
|
|
226
|
+
const remoteOrder = createRemoteModule(oldOrderModule, 1000);
|
|
227
|
+
const manifest = createManifest([remoteUser, remoteOrder], 1000);
|
|
228
|
+
writeLocalData(dataDir, [userModule, newOrderModule], 3000);
|
|
229
|
+
writeConfig(dataDir);
|
|
230
|
+
const requests = [];
|
|
231
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
232
|
+
requests.push(config);
|
|
233
|
+
if (config.method == 'GET') return { status: 200, data: jsonBuffer(manifest), headers: { etag: '"v1"' } };
|
|
234
|
+
return { status: 204, data: '', headers: {} };
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await service.push({ featureIds: ['feature-order'], force: true });
|
|
238
|
+
const manifestPut = requests.find(request => request.method == 'PUT' && request.url.endsWith(WEBDAV_FILE_NAME));
|
|
239
|
+
const nextManifest = JSON.parse(manifestPut.data.toString('utf8'));
|
|
240
|
+
expect(nextManifest.modules.map(moduleItem => moduleItem.name)).toEqual(['用户中心', '新订单中心']);
|
|
241
|
+
expect(nextManifest.modules[0].fileName).toBe(remoteUser.entry.fileName);
|
|
242
|
+
expect(manifestPut.headers['If-Match']).toBe('"v1"');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('已有清单没有并发校验头时要求确认后才能覆盖', async () => {
|
|
246
|
+
const dataDir = createDataDir();
|
|
247
|
+
dataDirs.push(dataDir);
|
|
248
|
+
const localModule = createModule('feature-local', '本地模块');
|
|
249
|
+
const remoteModule = createRemoteModule(createModule('feature-remote', '远端模块'), 1000);
|
|
250
|
+
writeLocalData(dataDir, [localModule], 3000);
|
|
251
|
+
writeConfig(dataDir);
|
|
252
|
+
const manifest = createManifest([remoteModule], 1000);
|
|
253
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
254
|
+
if (config.method == 'GET') return { status: 200, data: jsonBuffer(manifest), headers: {} };
|
|
255
|
+
return { status: 204, data: '', headers: {} };
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await expect(service.push({ featureIds: ['feature-local'] })).rejects.toMatchObject({
|
|
259
|
+
statusCode: 409,
|
|
260
|
+
conflict: 'REMOTE_UNVERIFIABLE',
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('模块哈希基线避免其他模块修改掩盖远端冲突', async () => {
|
|
265
|
+
const dataDir = createDataDir();
|
|
266
|
+
dataDirs.push(dataDir);
|
|
267
|
+
const localUser = createModule('feature-user', '本地用户中心');
|
|
268
|
+
const localOrder = createModule('feature-order', '本地订单中心');
|
|
269
|
+
const localOrderHash = createRemoteModule(localOrder, 1000).entry.contentSha256;
|
|
270
|
+
const remoteOrder = createRemoteModule(createModule('feature-order', '远端订单中心'), 1000);
|
|
271
|
+
writeLocalData(dataDir, [localUser, localOrder], 5000);
|
|
272
|
+
writeConfig(dataDir);
|
|
273
|
+
fs.writeJsonSync(path.join(dataDir, 'webdav-sync-state.json'), {
|
|
274
|
+
endpoint: 'https://webdav.example.com/webdav/whistle-mock-plugin-sync/',
|
|
275
|
+
modules: { 'feature-order': localOrderHash },
|
|
276
|
+
});
|
|
277
|
+
const manifest = createManifest([remoteOrder], 1000);
|
|
278
|
+
const requests = [];
|
|
279
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
280
|
+
requests.push(config);
|
|
281
|
+
return { status: 200, data: jsonBuffer(manifest), headers: { etag: '"v2"' } };
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await expect(service.push({ featureIds: ['feature-order'] })).rejects.toMatchObject({
|
|
285
|
+
statusCode: 409,
|
|
286
|
+
conflict: 'REMOTE_NEWER',
|
|
287
|
+
});
|
|
288
|
+
expect(requests.some(request => request.method == 'PUT')).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('任一模块上传失败时不发布新清单', async () => {
|
|
292
|
+
const dataDir = createDataDir();
|
|
293
|
+
dataDirs.push(dataDir);
|
|
294
|
+
writeLocalData(dataDir, [createModule('feature-1', '模块一')]);
|
|
295
|
+
writeConfig(dataDir);
|
|
296
|
+
const requests = [];
|
|
297
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
298
|
+
requests.push(config);
|
|
299
|
+
if (config.method == 'GET') return { status: 404, data: '', headers: {} };
|
|
300
|
+
if (config.method == 'PUT') {
|
|
301
|
+
const error = new Error('timeout');
|
|
302
|
+
error.code = 'ECONNABORTED';
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
return { status: 204, data: '', headers: {} };
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await expect(service.push()).rejects.toThrow('请求超时');
|
|
309
|
+
expect(requests.some(request => request.method == 'MOVE')).toBe(false);
|
|
310
|
+
expect(requests.some(request => request.method == 'DELETE')).toBe(true);
|
|
311
|
+
expect(requests.some(request => request.method == 'PUT' && request.url.endsWith(WEBDAV_FILE_NAME))).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('只拉取勾选模块,替换同 ID 模块并保留未选本地模块', async () => {
|
|
315
|
+
const dataDir = createDataDir();
|
|
316
|
+
dataDirs.push(dataDir);
|
|
317
|
+
const localUser = createModule('feature-user', '本地用户中心');
|
|
318
|
+
const localOrder = createModule('feature-order', '本地订单中心');
|
|
319
|
+
const remoteUserModule = createModule('feature-user', '远端用户中心', 'remote-user-interface');
|
|
320
|
+
const remoteOrderModule = createModule('feature-order', '远端订单中心', 'remote-order-interface');
|
|
321
|
+
const remoteUser = createRemoteModule(remoteUserModule, 4000);
|
|
322
|
+
const remoteOrder = createRemoteModule(remoteOrderModule, 4000);
|
|
323
|
+
const manifest = createManifest([remoteUser, remoteOrder], 4000);
|
|
324
|
+
const files = writeLocalData(dataDir, [localUser, localOrder], 1000);
|
|
325
|
+
writeConfig(dataDir);
|
|
326
|
+
const requests = [];
|
|
327
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
328
|
+
requests.push(config);
|
|
329
|
+
if (config.url.endsWith(WEBDAV_FILE_NAME)) {
|
|
330
|
+
return { status: 200, data: jsonBuffer(manifest), headers: { etag: '"v1"' } };
|
|
331
|
+
}
|
|
332
|
+
if (config.url.endsWith(remoteUser.entry.fileName)) {
|
|
333
|
+
return { status: 200, data: remoteUser.compressed, headers: {} };
|
|
334
|
+
}
|
|
335
|
+
return { status: 404, data: '', headers: {} };
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await service.pull({ featureIds: ['feature-user'], force: true });
|
|
339
|
+
const features = fs.readJsonSync(files.featuresFile).features;
|
|
340
|
+
const interfaces = fs.readJsonSync(files.interfacesFile).interfaces;
|
|
341
|
+
expect(features.map(feature => feature.name)).toEqual(['远端用户中心', '本地订单中心']);
|
|
342
|
+
expect(interfaces.map(interfaceItem => interfaceItem.id)).toEqual(['feature-order-interface', 'remote-user-interface']);
|
|
343
|
+
expect(requests.some(request => request.url.endsWith(remoteOrder.entry.fileName))).toBe(false);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('分片损坏时不修改任何本地文件', async () => {
|
|
347
|
+
const dataDir = createDataDir();
|
|
348
|
+
dataDirs.push(dataDir);
|
|
349
|
+
const localModule = createModule('feature-user', '本地用户中心');
|
|
350
|
+
const remoteModule = createRemoteModule(createModule('feature-user', '远端用户中心'), 4000);
|
|
351
|
+
const manifest = createManifest([remoteModule], 4000);
|
|
352
|
+
const files = writeLocalData(dataDir, [localModule], 1000);
|
|
353
|
+
writeConfig(dataDir);
|
|
354
|
+
const beforeFeatures = fs.readFileSync(files.featuresFile, 'utf8');
|
|
355
|
+
const beforeInterfaces = fs.readFileSync(files.interfacesFile, 'utf8');
|
|
356
|
+
const service = createWebDavSync(dataDir, async (config) => {
|
|
357
|
+
if (config.url.endsWith(WEBDAV_FILE_NAME)) return { status: 200, data: jsonBuffer(manifest), headers: {} };
|
|
358
|
+
return { status: 200, data: Buffer.from('not-gzip'), headers: {} };
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await expect(service.pull({ force: true })).rejects.toThrow('不是有效的 gzip');
|
|
362
|
+
expect(fs.readFileSync(files.featuresFile, 'utf8')).toBe(beforeFeatures);
|
|
363
|
+
expect(fs.readFileSync(files.interfacesFile, 'utf8')).toBe(beforeInterfaces);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('下载期间保留未选模块修改,并拒绝覆盖发生变化的已选模块', async () => {
|
|
367
|
+
const dataDir = createDataDir();
|
|
368
|
+
dataDirs.push(dataDir);
|
|
369
|
+
const localUser = createModule('feature-user', '本地用户中心');
|
|
370
|
+
const localOrder = createModule('feature-order', '本地订单中心');
|
|
371
|
+
const remoteUser = createRemoteModule(createModule('feature-user', '远端用户中心'), 4000);
|
|
372
|
+
const manifest = createManifest([remoteUser], 4000);
|
|
373
|
+
const files = writeLocalData(dataDir, [localUser, localOrder], 1000);
|
|
374
|
+
writeConfig(dataDir);
|
|
375
|
+
let modifySelected = false;
|
|
376
|
+
const createService = () => createWebDavSync(dataDir, async (config) => {
|
|
377
|
+
if (config.url.endsWith(WEBDAV_FILE_NAME)) return { status: 200, data: jsonBuffer(manifest), headers: {} };
|
|
378
|
+
const currentFeatures = fs.readJsonSync(files.featuresFile).features;
|
|
379
|
+
currentFeatures.find(feature => feature.id == (modifySelected ? 'feature-user' : 'feature-order')).description = '下载期间修改';
|
|
380
|
+
fs.writeJsonSync(files.featuresFile, { features: currentFeatures });
|
|
381
|
+
return { status: 200, data: remoteUser.compressed, headers: {} };
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await createService().pull({ featureIds: ['feature-user'], force: true });
|
|
385
|
+
expect(fs.readJsonSync(files.featuresFile).features.find(feature => feature.id == 'feature-order').description)
|
|
386
|
+
.toBe('下载期间修改');
|
|
387
|
+
|
|
388
|
+
writeLocalData(dataDir, [localUser, localOrder], 1000);
|
|
389
|
+
modifySelected = true;
|
|
390
|
+
const beforeInterfaces = fs.readFileSync(files.interfacesFile, 'utf8');
|
|
391
|
+
await expect(createService().pull({ featureIds: ['feature-user'], force: true })).rejects.toMatchObject({
|
|
392
|
+
statusCode: 409,
|
|
393
|
+
conflict: 'LOCAL_CHANGED',
|
|
394
|
+
});
|
|
395
|
+
expect(fs.readJsonSync(files.featuresFile).features.find(feature => feature.id == 'feature-user').description)
|
|
396
|
+
.toBe('下载期间修改');
|
|
397
|
+
expect(fs.readFileSync(files.interfacesFile, 'utf8')).toBe(beforeInterfaces);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test('空选择和未知模块被拒绝', async () => {
|
|
401
|
+
const dataDir = createDataDir();
|
|
402
|
+
dataDirs.push(dataDir);
|
|
403
|
+
writeLocalData(dataDir, [createModule('feature-1', '功能')]);
|
|
404
|
+
writeConfig(dataDir);
|
|
405
|
+
const service = createWebDavSync(dataDir, async () => ({ status: 404, data: '', headers: {} }));
|
|
406
|
+
await expect(service.push({ featureIds: [] })).rejects.toThrow('至少选择一个');
|
|
407
|
+
await expect(service.push({ featureIds: ['missing'] })).rejects.toThrow('找不到要推送');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('设置默认关闭,关闭功能保留凭据;断开只删除本地凭据', () => {
|
|
411
|
+
const dataDir = createDataDir();
|
|
412
|
+
dataDirs.push(dataDir);
|
|
413
|
+
writeConfig(dataDir);
|
|
414
|
+
const getResponse = createResponse();
|
|
415
|
+
settingsController.call({ dataDir }, { method: 'GET' }, getResponse);
|
|
416
|
+
expect(getResponse.body.webDavSyncEnabled).toBe(false);
|
|
417
|
+
|
|
418
|
+
const putResponse = createResponse();
|
|
419
|
+
settingsController.call({ dataDir }, { method: 'PUT', body: { webDavSyncEnabled: false } }, putResponse);
|
|
420
|
+
expect(fs.existsSync(path.join(dataDir, 'webdav-config.json'))).toBe(true);
|
|
421
|
+
|
|
422
|
+
const httpClient = jest.fn();
|
|
423
|
+
const status = createWebDavSync(dataDir, httpClient).disconnect();
|
|
424
|
+
expect(status.configured).toBe(false);
|
|
425
|
+
expect(fs.existsSync(path.join(dataDir, 'webdav-config.json'))).toBe(false);
|
|
426
|
+
expect(httpClient).not.toHaveBeenCalled();
|
|
427
|
+
});
|
|
428
|
+
});
|