Skip to content

Commit 73d8c77

Browse files
authoredJan 5, 2023
RD-4792 RD-4793 Add snapshot support for page groups and widgets (#2355)
1 parent 1b0dbb4 commit 73d8c77

14 files changed

+277
-99
lines changed
 

‎backend/handler/templates/PageGroupsHandler.ts

+25-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import fs, { readdirSync, readJsonSync } from 'fs-extra';
55
import moment from 'moment';
66
import { builtInTemplatesFolder, userTemplatesFolder } from './TemplatesHandler';
77
import { getLogger } from '../LoggerHandler';
8-
import type { PageGroup, PageGroupFileContent, CreatePageGroupData } from './types';
8+
import type { CreatePageGroupData, PageGroup, PageGroupFileContent } from './types';
99

1010
const logger = getLogger('PageGroupsHandler');
1111

@@ -36,34 +36,50 @@ function getPageGroups(path: string, custom: boolean) {
3636
.value();
3737
}
3838

39+
export function getUserPageGroups() {
40+
return getPageGroups(userPageGroupsFolder, true);
41+
}
42+
3943
export function listPageGroups() {
40-
return [...getPageGroups(builtInPageGroupsDir, false), ...getPageGroups(userPageGroupsFolder, true)];
44+
return [...getPageGroups(builtInPageGroupsDir, false), ...getUserPageGroups()];
4145
}
4246

43-
export function createPageGroup(username: string, pageGroup: CreatePageGroupData) {
44-
const path = pathlib.resolve(userPageGroupsFolder, `${pageGroup.id}.json`);
47+
function getUserPageGroupPath(id: string) {
48+
return pathlib.resolve(userPageGroupsFolder, `${id}.json`);
49+
}
50+
51+
export function checkPageGroupExists(pageGroup: CreatePageGroupData) {
52+
const path = getUserPageGroupPath(pageGroup.id);
4553
if (fs.existsSync(path)) {
4654
return Promise.reject(`Page group id "${pageGroup.id}" already exists`);
4755
}
56+
return Promise.resolve();
57+
}
4858

59+
export function createPageGroup(pageGroup: CreatePageGroupData, updatedBy: string, updatedAt = moment().format()) {
60+
const path = getUserPageGroupPath(pageGroup.id);
4961
const content: PageGroupFileContent = {
5062
..._.omit(pageGroup, 'id'),
51-
updatedBy: username,
52-
updatedAt: moment().format()
63+
updatedBy,
64+
updatedAt
5365
};
5466

5567
return fs.writeJson(path, content, { spaces: ' ' });
5668
}
5769

70+
export function validateAndCreatePageGroup(username: string, pageGroup: CreatePageGroupData) {
71+
return checkPageGroupExists(pageGroup).then(() => createPageGroup(pageGroup, username));
72+
}
73+
5874
export function deletePageGroup(pageGroupId: string) {
59-
const path = pathlib.resolve(userPageGroupsFolder, `${pageGroupId}.json`);
75+
const path = getUserPageGroupPath(pageGroupId);
6076

6177
return fs.remove(path);
6278
}
6379

6480
export function updatePageGroup(username: string, id: string, pageGroup: CreatePageGroupData) {
65-
const existingFilePath = pathlib.resolve(userPageGroupsFolder, `${id}.json`);
66-
const newFilePath = pathlib.resolve(userPageGroupsFolder, `${pageGroup.id}.json`);
81+
const existingFilePath = getUserPageGroupPath(id);
82+
const newFilePath = getUserPageGroupPath(pageGroup.id);
6783

6884
const content: PageGroupFileContent = {
6985
..._.omit(pageGroup, 'id'),

‎backend/handler/WidgetsHandler.ts ‎backend/handler/widgets/WidgetsHandler.ts

+44-65
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,42 @@ import mkdirp from 'mkdirp';
55
import os from 'os';
66
import pathlib from 'path';
77

8-
import { getConfig } from '../config';
9-
import { db } from '../db/Connection';
10-
import type { UserAppsInstance } from '../db/models/UserAppsModel';
11-
import { getResourcePath } from '../utils';
12-
import * as ArchiveHelper from './ArchiveHelper';
13-
import * as BackendHandler from './BackendHandler';
14-
15-
import { getLogger } from './LoggerHandler';
16-
import type { WidgetData, WidgetUsage } from './WidgetsHandler.types';
8+
import archiver from 'archiver';
9+
import { getConfig } from '../../config';
10+
import { db } from '../../db/Connection';
11+
import type { UserAppsInstance } from '../../db/models/UserAppsModel';
12+
import { getResourcePath } from '../../utils';
13+
import * as ArchiveHelper from '../ArchiveHelper';
14+
import * as BackendHandler from '../BackendHandler';
15+
16+
import { getLogger } from '../LoggerHandler';
17+
import type { WidgetData, WidgetUsage } from '../WidgetsHandler.types';
18+
import validateUniqueness from './validateUniqueness';
19+
import installFiles from './installFiles';
1720

1821
const logger = getLogger('WidgetHandler');
1922

2023
const builtInWidgetsFolder = getResourcePath('widgets', false);
21-
const userWidgetsFolder = getResourcePath('widgets', true);
24+
export const userWidgetsFolder = getResourcePath('widgets', true);
2225
const widgetTempDir = pathlib.join(os.tmpdir(), getConfig().app.widgets.tempDir);
2326

24-
function saveMultipartData(req: Request) {
27+
function saveMultipartData(req: Request, multipartId = 'widget') {
2528
const targetPath = pathlib.join(widgetTempDir, `widget${Date.now()}`);
26-
return ArchiveHelper.saveMultipartData(req, targetPath, 'widget');
29+
return ArchiveHelper.saveMultipartData(req, targetPath, multipartId);
2730
}
2831

2932
function saveDataFromUrl(archiveUrl: string) {
3033
const targetPath = pathlib.join(widgetTempDir, `widget${Date.now()}`);
3134
return ArchiveHelper.saveDataFromUrl(archiveUrl, targetPath);
3235
}
3336

34-
// Credits to: https://geedew.com/remove-a-directory-that-is-not-empty-in-nodejs/
35-
36-
function rmdirSync(path: string) {
37-
if (fs.existsSync(path)) {
38-
fs.readdirSync(path).forEach(file => {
39-
const curPath = `${path}/${file}`;
40-
if (fs.lstatSync(curPath).isDirectory()) {
41-
// recurse
42-
rmdirSync(curPath);
43-
} else {
44-
// delete file
45-
fs.unlinkSync(curPath);
46-
}
47-
});
48-
fs.rmdirSync(path);
49-
}
50-
}
51-
52-
function getUserWidgets() {
37+
export function getUserWidgets() {
5338
return fs
5439
.readdirSync(pathlib.resolve(userWidgetsFolder))
5540
.filter(dir => fs.lstatSync(pathlib.resolve(userWidgetsFolder, dir)).isDirectory());
5641
}
5742

58-
function getBuiltInWidgets() {
43+
export function getBuiltInWidgets() {
5944
return fs
6045
.readdirSync(pathlib.resolve(builtInWidgetsFolder))
6146
.filter(
@@ -65,21 +50,6 @@ function getBuiltInWidgets() {
6550
);
6651
}
6752

68-
function getAllWidgets() {
69-
return _.concat(getBuiltInWidgets(), getUserWidgets());
70-
}
71-
72-
function validateUniqueness(widgetId: string) {
73-
logger.debug(`Validating widget ${widgetId} uniqueness.`);
74-
75-
const widgets = getAllWidgets();
76-
if (_.indexOf(widgets, widgetId) >= 0) {
77-
return Promise.reject({ status: 422, message: `Widget ${widgetId} is already installed` });
78-
}
79-
80-
return Promise.resolve();
81-
}
82-
8353
function validateConsistency(widgetId: string, dirName: string) {
8454
logger.debug(`Validating widget ${widgetId} consistency.`);
8555

@@ -129,24 +99,6 @@ function validateWidget(widgetId: string, extractedDir: string) {
12999
return Promise.resolve(tempPath);
130100
}
131101

132-
function installFiles(widgetId: string, tempPath: string) {
133-
logger.debug('Installing widget files to the target path:', pathlib.resolve(userWidgetsFolder));
134-
logger.debug('Widget temp path:', tempPath);
135-
136-
const installPath = pathlib.resolve(userWidgetsFolder, widgetId);
137-
138-
return new Promise<void>((resolve, reject) => {
139-
rmdirSync(installPath);
140-
fs.move(tempPath, installPath, err => {
141-
if (err) {
142-
reject(err);
143-
} else {
144-
resolve();
145-
}
146-
});
147-
});
148-
}
149-
150102
function backupWidget(widgetId: string, tempPath: string) {
151103
const installPath = pathlib.resolve(userWidgetsFolder, widgetId);
152104
const backupPath = pathlib.resolve(tempPath, 'backup');
@@ -311,3 +263,30 @@ export function init() {
311263
}
312264
});
313265
}
266+
267+
export function createUserWidgetsSnapshot(onError: (err?: any) => void) {
268+
const archive = archiver('zip');
269+
archive.directory(userWidgetsFolder, false);
270+
archive.finalize().catch(onError);
271+
return archive;
272+
}
273+
274+
export function restoreUserWidgetsSnapshot(req: Request) {
275+
return saveMultipartData(req, 'snapshot').then(({ archiveFolder, archiveFile }) => {
276+
const archivePath = pathlib.join(archiveFolder, archiveFile);
277+
const extractDirPath = pathlib.join(archiveFolder, 'extract');
278+
279+
return ArchiveHelper.decompressArchive(archivePath, extractDirPath).then(() => {
280+
const widgetIds = fs.readdirSync(pathlib.resolve(extractDirPath));
281+
return Promise.all(widgetIds.map(widgetId => validateUniqueness(widgetId))).then(() =>
282+
Promise.all(
283+
widgetIds.map(widgetId =>
284+
installFiles(widgetId, pathlib.join(extractDirPath, widgetId)).then(() =>
285+
BackendHandler.importWidgetBackend(widgetId)
286+
)
287+
)
288+
)
289+
);
290+
});
291+
});
292+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import pathlib from 'path';
2+
import fs from 'fs-extra';
3+
import { userWidgetsFolder } from './WidgetsHandler';
4+
5+
function rmdirSync(path: string) {
6+
if (fs.existsSync(path)) {
7+
fs.readdirSync(path).forEach(file => {
8+
const curPath = `${path}/${file}`;
9+
if (fs.lstatSync(curPath).isDirectory()) {
10+
// recurse
11+
rmdirSync(curPath);
12+
} else {
13+
// delete file
14+
fs.unlinkSync(curPath);
15+
}
16+
});
17+
fs.rmdirSync(path);
18+
}
19+
}
20+
21+
export default function installFiles(widgetId: string, tempPath: string) {
22+
const installPath = pathlib.resolve(userWidgetsFolder, widgetId);
23+
24+
return new Promise<void>((resolve, reject) => {
25+
rmdirSync(installPath);
26+
fs.move(tempPath, installPath, err => {
27+
if (err) {
28+
reject(err);
29+
} else {
30+
resolve();
31+
}
32+
});
33+
});
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import _ from 'lodash';
2+
import { getBuiltInWidgets, getUserWidgets } from './WidgetsHandler';
3+
4+
function getAllWidgets() {
5+
return _.concat(getBuiltInWidgets(), getUserWidgets());
6+
}
7+
8+
export default function validateUniqueness(widgetId: string) {
9+
const widgets = getAllWidgets();
10+
if (_.indexOf(widgets, widgetId) >= 0) {
11+
return Promise.reject({ status: 422, message: `Widget ${widgetId} is already installed` });
12+
}
13+
14+
return Promise.resolve();
15+
}

‎backend/routes/Snapshots.ts

+46-12
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { omit } from 'lodash';
44
import { db } from '../db/Connection';
55
import type { UserAppsInstance } from '../db/models/UserAppsModel';
66
import type { UserAppsAttributes } from '../db/models/UserAppsModel.types';
7-
import type { Page, Template } from '../handler/templates/types';
7+
import type { Page, PageGroup, Template } from '../handler/templates/types';
88
import { checkTemplateExists, createTemplate, getUserTemplates } from '../handler/templates/TemplatesHandler';
99
import { checkPageExists, createPage, getUserPages } from '../handler/templates/PagesHandler';
10+
import { checkPageGroupExists, createPageGroup, getUserPageGroups } from '../handler/templates/PageGroupsHandler';
11+
import { createUserWidgetsSnapshot, restoreUserWidgetsSnapshot } from '../handler/widgets/WidgetsHandler';
1012

1113
const router = express.Router();
1214

@@ -51,13 +53,14 @@ router.use(express.json());
5153
});
5254
})();
5355

54-
(function templates() {
55-
const propertiesToOmit = ['name', 'custom'] as const;
56-
type PropertiesToOmit = typeof propertiesToOmit[number];
57-
type TemplatesSnapshot = Omit<Template, PropertiesToOmit>[];
56+
const commonPropertiesToOmit = ['custom'] as const;
57+
type CommonPropertiesToOmit = typeof commonPropertiesToOmit[number];
5858

59+
const templatePropertiesToOmit = [...commonPropertiesToOmit, 'name'] as const;
60+
export type TemplatesSnapshot = Omit<Template, typeof templatePropertiesToOmit[number]>[];
61+
(() => {
5962
router.get('/templates', (_req, res: Response<TemplatesSnapshot>, _next) => {
60-
res.send(getUserTemplates().map(template => omit(template, propertiesToOmit)));
63+
res.send(getUserTemplates().map(template => omit(template, templatePropertiesToOmit)));
6164
});
6265

6366
router.post<never, never, TemplatesSnapshot>('/templates', (req, res, next) => {
@@ -79,13 +82,10 @@ router.use(express.json());
7982
});
8083
})();
8184

82-
(function pages() {
83-
const propertiesToOmit = ['custom'] as const;
84-
type PropertiesToOmit = typeof propertiesToOmit[number];
85-
type PagesSnapshot = Omit<Page, PropertiesToOmit>[];
86-
85+
export type PagesSnapshot = Omit<Page, CommonPropertiesToOmit>[];
86+
(() => {
8787
router.get('/pages', (_req, res: Response<PagesSnapshot>, _next) => {
88-
res.send(getUserPages().map(page => omit(page, propertiesToOmit)));
88+
res.send(getUserPages().map(page => omit(page, commonPropertiesToOmit)));
8989
});
9090

9191
router.post<never, never, PagesSnapshot>('/pages', (req, res, next) => {
@@ -103,4 +103,38 @@ router.use(express.json());
103103
});
104104
})();
105105

106+
export type PageGroupsSnapshot = Omit<PageGroup, CommonPropertiesToOmit>[];
107+
(() => {
108+
router.get('/page-groups', (_req, res: Response<PageGroupsSnapshot>, _next) => {
109+
res.send(getUserPageGroups().map(pageGroup => omit(pageGroup, commonPropertiesToOmit)));
110+
});
111+
112+
router.post<never, never, PageGroupsSnapshot>('/page-groups', (req, res, next) => {
113+
Promise.all(req.body.map(pageGroupData => checkPageGroupExists(pageGroupData)))
114+
.catch(message => Promise.reject({ message, status: 400 }))
115+
.then(() =>
116+
Promise.all(
117+
req.body.map(pageGroupData =>
118+
createPageGroup(pageGroupData, pageGroupData.updatedBy, pageGroupData.updatedAt)
119+
)
120+
)
121+
)
122+
.then(() => res.status(201).end())
123+
.catch(next);
124+
});
125+
})();
126+
127+
(() => {
128+
router.get('/widgets', (_req, res, next) => {
129+
const snapshot = createUserWidgetsSnapshot(next);
130+
snapshot.pipe(res);
131+
});
132+
133+
router.post('/widgets', (req, res, next) => {
134+
restoreUserWidgetsSnapshot(req)
135+
.then(() => res.status(201).end())
136+
.catch(next);
137+
});
138+
})();
139+
106140
export default router;

‎backend/routes/Templates.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ router.get('/page-groups', (_req, res: Response<GetPageGroupsResponse>, next) =>
6565
});
6666

6767
router.post<never, never, PostPageGroupsRequestBody>('/page-groups', (req, res, next) => {
68-
PageGroupsHandler.createPageGroup(req.user!.username, req.body)
68+
PageGroupsHandler.validateAndCreatePageGroup(req.user!.username, req.body)
6969
.then(() => res.status(200).end())
7070
.catch(next);
7171
});

‎backend/routes/Widgets.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import express from 'express';
21
import type { RequestHandler, Response } from 'express';
3-
import * as WidgetsHandler from '../handler/WidgetsHandler';
2+
import express from 'express';
3+
import * as WidgetsHandler from '../handler/widgets/WidgetsHandler';
44
import { getRBAC, isAuthorized } from '../handler/AuthHandler';
55
import { getTokenFromCookies } from '../utils';
66
import type {

‎backend/server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { Server } from 'http';
22
import app from './app';
33
import DBConnection from './db/Connection';
4-
import { init as initWidgetsHandler } from './handler/WidgetsHandler';
4+
import { init as initWidgetsHandler } from './handler/widgets/WidgetsHandler';
55
import { init as initTemplatesHandler } from './handler/templates';
66
import { getLogger } from './handler/LoggerHandler';
77
import { isDevelopmentOrTest } from './utils';
88

9-
import { init, getMode } from './serverSettings';
9+
import { getMode, init } from './serverSettings';
1010
import { getBackendConfig } from './config';
1111

1212
const logger = getLogger('Server');

‎backend/test/handlers/templates/PageGroupsHandler.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ describe('PageGroupsHandler', () => {
3232
]);
3333
});
3434

35-
it('should create page group', () => {
36-
PageGroupsHandler.createPageGroup('admin', { id: 'pg', name: 'pg', icon: 'stop', pages: [] });
35+
it('should create page group', async () => {
36+
await PageGroupsHandler.validateAndCreatePageGroup('admin', { id: 'pg', name: 'pg', icon: 'stop', pages: [] });
3737

3838
expect(writeJson).toHaveBeenCalledWith(
3939
pathlib.resolve(userTemplatesFolder, 'page-groups', 'pg.json'),

‎backend/test/handlers/WidgetHandler.test.ts ‎backend/test/handlers/widgets/WidgetHandler.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import mkdirp from 'mkdirp';
22
import { getResourcePath } from 'utils';
3-
import { listWidgets } from 'handler/WidgetsHandler';
3+
import { listWidgets } from 'handler/widgets/WidgetsHandler';
44

55
jest.mock('handler/ManagerHandler');
66

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import installFiles from '../../../handler/widgets/installFiles';
2+
3+
describe('installFiles', () => {
4+
it('rejects on invalid source path', () => expect(installFiles('', '')).rejects.toBeTruthy());
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import validateUniqueness from '../../../handler/widgets/validateUniqueness';
2+
3+
describe('validateUniqueness', () => {
4+
it('resolves when widget does not exist', () => validateUniqueness('nonExistingWidgetId'));
5+
});

‎backend/test/routes/Snapshots.test.ts

+95-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ import app from 'app';
33
import { createTemplate, getUserTemplates } from 'handler/templates/TemplatesHandler';
44
import { omit, pick } from 'lodash';
55
import { createPage, getUserPages } from 'handler/templates/PagesHandler';
6-
import type { Page, Template } from 'handler/templates/types';
6+
import type { Page, PageGroup, Template } from 'handler/templates/types';
7+
import { createPageGroup, getUserPageGroups } from 'handler/templates/PageGroupsHandler';
8+
import validateUniqueness from 'handler/widgets/validateUniqueness';
9+
import installFiles from 'handler/widgets/installFiles';
10+
import decompress from 'decompress';
11+
import path from 'path';
12+
import { importWidgetBackend } from 'handler/BackendHandler';
713
import mockDb from '../mockDb';
14+
import type { PageGroupsSnapshot, PagesSnapshot, TemplatesSnapshot } from '../../routes/Snapshots';
815

916
jest.mock('db/Connection');
1017
jest.mock('handler/templates/TemplatesHandler');
1118
jest.mock('handler/templates/PagesHandler');
19+
jest.mock('handler/templates/PageGroupsHandler');
20+
jest.mock('handler/widgets/validateUniqueness');
21+
jest.mock('handler/widgets/installFiles');
22+
jest.mock('handler/BackendHandler');
1223

1324
describe('/snapshots/ua endpoint', () => {
1425
const userAppRow = {
@@ -90,21 +101,23 @@ describe('/snapshots/templates endpoint', () => {
90101
updatedAt: '2022-06-01T18:30:57.450Z'
91102
};
92103

104+
const templatesSnapshot: TemplatesSnapshot = [omit(template, 'name', 'custom')];
105+
93106
it('allows to get snapshot data', () => {
94107
(<jest.Mock>getUserTemplates).mockReturnValue([template]);
95108

96109
return request(app)
97110
.get('/console/snapshots/templates')
98111
.then(response => {
99112
expect(response.statusCode).toBe(200);
100-
expect(response.body).toStrictEqual([omit(template, 'name', 'custom')]);
113+
expect(response.body).toStrictEqual(templatesSnapshot);
101114
});
102115
});
103116

104117
it('allows to restore snapshot data', () => {
105118
return request(app)
106119
.post('/console/snapshots/templates')
107-
.send([omit(template, 'name', 'custom')])
120+
.send(templatesSnapshot)
108121
.then(response => {
109122
expect(response.statusCode).toBe(201);
110123
expect(createTemplate).toHaveBeenCalledWith(
@@ -132,21 +145,23 @@ describe('/snapshots/pages endpoint', () => {
132145
updatedAt: '2022-06-01T18:30:57.450Z'
133146
};
134147

148+
const pagesSnapshot: PagesSnapshot = [omit(page, 'custom')];
149+
135150
it('allows to get snapshot data', () => {
136151
(<jest.Mock>getUserPages).mockReturnValue([page]);
137152

138153
return request(app)
139154
.get('/console/snapshots/pages')
140155
.then(response => {
141156
expect(response.statusCode).toBe(200);
142-
expect(response.body).toStrictEqual([omit(page, 'custom')]);
157+
expect(response.body).toStrictEqual(pagesSnapshot);
143158
});
144159
});
145160

146161
it('allows to restore snapshot data', () => {
147162
return request(app)
148163
.post('/console/snapshots/pages')
149-
.send([omit(page, 'custom')])
164+
.send(pagesSnapshot)
150165
.then(response => {
151166
expect(response.statusCode).toBe(201);
152167
expect(createPage).toHaveBeenCalledWith(
@@ -160,3 +175,78 @@ describe('/snapshots/pages endpoint', () => {
160175
});
161176
});
162177
});
178+
179+
describe('/snapshots/page-groups endpoint', () => {
180+
const pageGroup: PageGroup = {
181+
id: 'test',
182+
name: 'test',
183+
custom: true,
184+
pages: ['adminDash'],
185+
icon: 'rocket',
186+
updatedBy: 'user',
187+
updatedAt: '2022-06-01T18:30:57.450Z'
188+
};
189+
190+
const pageGroupsSnapshot: PageGroupsSnapshot = [omit(pageGroup, 'custom')];
191+
192+
it('allows to get snapshot data', () => {
193+
(<jest.Mock>getUserPageGroups).mockReturnValue([pageGroup]);
194+
195+
return request(app)
196+
.get('/console/snapshots/page-groups')
197+
.then(response => {
198+
expect(response.statusCode).toBe(200);
199+
expect(response.body).toStrictEqual(pageGroupsSnapshot);
200+
});
201+
});
202+
203+
it('allows to restore snapshot data', () => {
204+
return request(app)
205+
.post('/console/snapshots/page-groups')
206+
.send(pageGroupsSnapshot)
207+
.then(response => {
208+
expect(response.statusCode).toBe(201);
209+
expect(createPageGroup).toHaveBeenCalledWith(
210+
expect.objectContaining(pick(pageGroup, 'name', 'pages', 'icon')),
211+
pageGroup.updatedBy,
212+
pageGroup.updatedAt
213+
);
214+
});
215+
});
216+
});
217+
218+
describe('/snapshots/widgets endpoint', () => {
219+
it('allows to get snapshot data', () => {
220+
return request(app)
221+
.get('/console/snapshots/widgets')
222+
.responseType('blob')
223+
.then(response => {
224+
expect(response.statusCode).toBe(200);
225+
return decompress(response.body);
226+
});
227+
});
228+
229+
it('allows to restore snapshot data', () => {
230+
(<jest.Mock>validateUniqueness).mockResolvedValue(null);
231+
(<jest.Mock>importWidgetBackend).mockResolvedValue(null);
232+
(<jest.Mock>installFiles).mockResolvedValue(null);
233+
return request(app)
234+
.post('/console/snapshots/widgets')
235+
.attach('snapshot', path.join(__dirname, 'fixtures/snapshots/widgets.zip'))
236+
.then(response => {
237+
expect(response.statusCode).toBe(201);
238+
expect(validateUniqueness).toHaveBeenCalledTimes(2);
239+
expect(validateUniqueness).toHaveBeenCalledWith('testWidget');
240+
expect(validateUniqueness).toHaveBeenCalledWith('testWidgetBackend');
241+
expect(installFiles).toHaveBeenCalledTimes(2);
242+
expect(installFiles).toHaveBeenCalledWith('testWidget', expect.stringMatching('/testWidget$'));
243+
expect(installFiles).toHaveBeenCalledWith(
244+
'testWidgetBackend',
245+
expect.stringMatching('/testWidgetBackend')
246+
);
247+
expect(importWidgetBackend).toHaveBeenCalledTimes(2);
248+
expect(importWidgetBackend).toHaveBeenCalledWith('testWidget');
249+
expect(importWidgetBackend).toHaveBeenCalledWith('testWidgetBackend');
250+
});
251+
});
252+
});
Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.