From 32fac33d8d8e46b8dc90837f9e75bd589edcf88c Mon Sep 17 00:00:00 2001 From: gmzsenturk Date: Thu, 25 Feb 2021 15:29:23 +0300 Subject: [PATCH] OpenStackSwift tests added to project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can BilgiƧ --- .../techdocs-common/__mocks__/pkgcloud.ts | 41 +-- packages/techdocs-common/package.json | 1 + .../src/stages/publish/openStackSwift.test.ts | 248 ++++++++++++++++++ .../src/stages/publish/openStackSwift.ts | 8 +- yarn.lock | 181 +++++++++++-- 5 files changed, 424 insertions(+), 55 deletions(-) create mode 100644 packages/techdocs-common/src/stages/publish/openStackSwift.test.ts diff --git a/packages/techdocs-common/__mocks__/pkgcloud.ts b/packages/techdocs-common/__mocks__/pkgcloud.ts index 3ea3c251e0..bf729a7bd2 100644 --- a/packages/techdocs-common/__mocks__/pkgcloud.ts +++ b/packages/techdocs-common/__mocks__/pkgcloud.ts @@ -13,21 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OpenstackProviderOptions } from 'pkgcloud'; import fs from 'fs-extra'; import os from 'os'; import path from 'path'; +import { ObjectWritableMock, BufferReadableMock } from 'stream-mock'; const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; const checkFileExists = async (Key: string): Promise => { // Key will always have / as file separator irrespective of OS since S3 expects /. // Normalize Key to OS specific path before checking if file exists. - const relativeFilePath = Key.split(path.posix.sep).join(path.sep); const filePath = path.join(rootDir, Key); try { - await fs.access(filePath, fs.constants.F_OK); + fs.accessSync(filePath, fs.constants.F_OK); return true; } catch (err) { return false; @@ -38,11 +37,11 @@ class PkgCloudStorageClient { getFile( containerName: string, file: string, - callback: (err: string, file: string) => any, + callback: (err: any, file: string) => any, ) { checkFileExists(file).then(res => { if (!res) { - callback('File does not exist', undefined); + callback('File does not exist', file); throw new Error('File does not exist'); } else { callback(undefined, 'success'); @@ -55,40 +54,28 @@ class PkgCloudStorageClient { callback: (err: string, container: string) => any, ) { if (containerName !== 'mock') { - callback("Container doesn't exist", undefined); + callback("Container doesn't exist", containerName); throw new Error('Container does not exist'); } else { - callback(undefined, 'success'); + callback('Container does not exist', 'success'); } } - upload({ containerName, remote }: { containerName: string; remote: string }) { - checkFileExists(remote).then(res => { - if (!res) { - return new Error("File doesn't exists"); - } - return fs.createWriteStream(`${containerName}/${remote}`); - }); + upload() { + return new ObjectWritableMock(); } - download({ - containerName, - remote, - }: { - containerName: string; - remote: string; - }) { - checkFileExists(remote).then(res => { - if (!res) { - return new Error("File doesn't exists"); - } - return fs.createReadStream(remote); + download() { + const stringify = JSON.stringify({ + "site_description": 'site_content', + "site_name": "backstage" }); + return new BufferReadableMock([stringify]); } } export class storage { - static createClient(params: OpenstackProviderOptions) { + static createClient() { return new PkgCloudStorageClient(); } } diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index a543addfa9..2598de1dce 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -58,6 +58,7 @@ "p-limit": "^3.1.0", "pkgcloud": "^2.2.0", "recursive-readdir": "^2.2.2", + "stream-mock": "^2.0.5", "winston": "^3.2.1" }, "devDependencies": { diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts new file mode 100644 index 0000000000..fab0bf3a12 --- /dev/null +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.test.ts @@ -0,0 +1,248 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Entity, + EntityName, + ENTITY_DEFAULT_NAMESPACE, +} from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import mockFs from 'mock-fs'; +import os from 'os'; +import path from 'path'; +import * as winston from 'winston'; +import { OpenStackSwiftPublish } from './openStackSwift'; +import { PublisherBase, TechDocsMetadata } from './types'; + +// NOTE: /packages/techdocs-common/__mocks__ is being used to mock pkgcloud client library + +const createMockEntity = (annotations = {}): Entity => { + return { + apiVersion: 'version', + kind: 'TestKind', + metadata: { + name: 'test-component-name', + namespace: 'test-namespace', + annotations: { + ...annotations, + }, + }, + }; +}; + +const createMockEntityName = (): EntityName => ({ + kind: 'TestKind', + name: 'test-component-name', + namespace: 'test-namespace', +}); + +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + +const getEntityRootDir = (entity: Entity) => { + const { + kind, + metadata: { namespace, name }, + } = entity; + + return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name); +}; + +const logger = winston.createLogger(); +jest.spyOn(logger, 'info').mockReturnValue(logger); +jest.spyOn(logger, 'error').mockReturnValue(logger); + +let publisher: PublisherBase; + +beforeEach(() => { + mockFs.restore(); + const mockConfig = new ConfigReader({ + techdocs: { + requestUrl: 'http://localhost:7000', + publisher: { + type: 'openStackSwift', + openStackSwift: { + username: 'mockuser', + password: 'verystrongpass', + authUrl: 'mockauthurl', + region: 'mockregion', + containerName: 'mock', + }, + }, + }, + }); + + publisher = OpenStackSwiftPublish.fromConfig(mockConfig, logger); +}); + +describe('OpenStackSwiftPublish', () => { + describe('publish', () => { + beforeEach(() => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'index.html': '', + '404.html': '', + assets: { + 'main.css': '', + }, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should publish a directory', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + setTimeout(async () => { + expect( + await publisher.publish({ + entity, + directory: entityRootDir, + }), + ).toBeUndefined() + }, 5000); + }); + + it('should fail to publish a directory', async () => { + expect.assertions(3); + const wrongPathToGeneratedDirectory = path.join( + rootDir, + 'wrong', + 'path', + 'to', + 'generatedDirectory', + ); + + const entity = createMockEntity(); + await expect( + publisher.publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }), + ).rejects.toThrowError(); + + await publisher + .publish({ + entity, + directory: wrongPathToGeneratedDirectory, + }) + .catch(error => { + expect(error.message).toEqual( + // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error + // Issue reported https://github.com/tschaub/mock-fs/issues/118 + expect.stringContaining( + `Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`, + ), + ); + expect(error.message).toEqual( + expect.stringContaining(wrongPathToGeneratedDirectory), + ); + }); + mockFs.restore(); + }); + }); + + describe('hasDocsBeenGenerated', () => { + it('should return true if docs has been generated', async () => { + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'index.html': 'file-content', + }, + }); + + expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true); + mockFs.restore(); + }); + + it('should return false if docs has not been generated', async () => { + const entity = createMockEntity(); + + expect(await publisher.hasDocsBeenGenerated(entity)).toBe(false); + }); + }); + + describe('fetchTechDocsMetadata', () => { + it('should return tech docs metadata', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': + '{"site_name": "backstage", "site_description": "site_content"}', + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return tech docs metadata when json encoded with single quotes', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + mockFs({ + [entityRootDir]: { + 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content'}`, + }, + }); + + const expectedMetadata: TechDocsMetadata = { + site_name: 'backstage', + site_description: 'site_content', + }; + expect( + await publisher.fetchTechDocsMetadata(entityNameMock), + ).toStrictEqual(expectedMetadata); + mockFs.restore(); + }); + + it('should return an error if the techdocs_metadata.json file is not present', async () => { + const entityNameMock = createMockEntityName(); + const entity = createMockEntity(); + const entityRootDir = getEntityRootDir(entity); + + await publisher + .fetchTechDocsMetadata(entityNameMock) + .catch(error => + expect(error).toEqual( + new Error( + `TechDocs metadata fetch failed, The file ${path.join( + entityRootDir, + 'techdocs_metadata.json', + )} does not exist !`, + ), + ), + ); + }); + }); +}); diff --git a/packages/techdocs-common/src/stages/publish/openStackSwift.ts b/packages/techdocs-common/src/stages/publish/openStackSwift.ts index 15d8b20332..60c8349dee 100644 --- a/packages/techdocs-common/src/stages/publish/openStackSwift.ts +++ b/packages/techdocs-common/src/stages/publish/openStackSwift.ts @@ -107,7 +107,6 @@ export class OpenStackSwiftPublish implements PublisherBase { * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html */ async publish({ entity, directory }: PublishRequest): Promise { - this.logger.info(`Publish Called hey`); try { // Note: OpenStack Swift manages creation of parent directories if they do not exist. // So collecting path of only the files is good enough. @@ -139,8 +138,7 @@ export class OpenStackSwiftPublish implements PublisherBase { }; // Rate limit the concurrent execution of file uploads to batches of 10 (per publish) - const uploadFile = limiter( - () => + const uploadFile = limiter(() => new Promise((res, rej) => { const writeStream = this.storageClient.upload(params); @@ -168,7 +166,6 @@ export class OpenStackSwiftPublish implements PublisherBase { async fetchTechDocsMetadata( entityName: EntityName, ): Promise { - this.logger.info(`fetchTechDocsMetadata Called hey`); try { return await new Promise(async (resolve, reject) => { const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; @@ -206,7 +203,6 @@ export class OpenStackSwiftPublish implements PublisherBase { */ docsRouter(): express.Handler { return async (req, res) => { - this.logger.info(`docsRouter Called hey`); // Trim the leading forward slash // filePath example - /default/Component/documented-component/index.html @@ -243,7 +239,6 @@ export class OpenStackSwiftPublish implements PublisherBase { */ async hasDocsBeenGenerated(entity: Entity): Promise { try { - this.logger.info(`hasDocsBeenGenerated Called hey`); const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; return new Promise(res => { @@ -251,7 +246,6 @@ export class OpenStackSwiftPublish implements PublisherBase { this.containerName, `${entityRootDir}/index.html`, (err: any, file: any) => { - console.log(file); if (!err && file) { res(true); } else res(false); diff --git a/yarn.lock b/yarn.lock index f941b3ee7c..5b7893c852 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1816,35 +1816,78 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.7.2" + version "0.2.0" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.2.0.tgz#e3fe2a4ddeb6a9b6ec480c80cb2b9c39cb245576" + integrity sha512-Y1ocdRpBlxK/VrJQjHlQd0bgADECd1B2NRjwd8ss46ibT5hwLvMOfD80+Fa7oPLu0ktJrH4lq0pNIIJIml48zA== dependencies: - "@backstage/config" "^0.1.3" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" - ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.7.2" + version "0.3.1" + resolved "https://registry.npmjs.org/@backstage/catalog-model/-/catalog-model-0.3.1.tgz#45d08e2f333c9c566b2bf2629fd707fe989bb404" + integrity sha512-9XhV7c4rmVW+Yzj2PiwTQ7DsegWGB3C4ELsDRExuEVZONdqNcC02cyJtrt3fT5F31ZS3tHkB9bMUymFOBLqUSA== dependencies: - "@backstage/config" "^0.1.3" + "@backstage/config" "^0.1.1" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" - ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.6.3" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.3.2.tgz#a8209126d5076cf4a8b9bd632fe4e5e2edb62916" + integrity sha512-i5d+Wh8js4qEWoAsPY5L7HVSWpumr1OhfF2dUCGYdyW6AMqVJPca6+n6zp1Rg2CO+J9norp44XAVVCbyhtUpig== dependencies: - "@backstage/config" "^0.1.3" - "@backstage/core-api" "^0.2.11" - "@backstage/theme" "^0.2.3" + "@backstage/config" "^0.1.1" + "@backstage/core-api" "^0.2.1" + "@backstage/theme" "^0.2.1" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@types/dagre" "^0.7.44" + "@types/react" "^16.9" + "@types/react-sparklines" "^1.7.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-zoom "^2.0.0" + dagre "^0.8.5" + immer "^7.0.9" + lodash "^4.17.15" + material-table "^1.69.1" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^6.6.0" + react-markdown "^5.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^13.5.1" + react-use "^15.3.3" + remark-gfm "^1.0.0" + zen-observable "^0.8.15" + +"@backstage/core@^0.5.0": + version "0.5.0" + resolved "https://registry.npmjs.org/@backstage/core/-/core-0.5.0.tgz#6ff384adc595c18c7db60b9b2d23ebbb9086ed36" + integrity sha512-lCxgKBavUlLYZjZmRF8A7koP4NUhK/tbdf9SaEod0miZg6JTaDoAm3dmHPyqrMBHgoRRCDTxRIxNhj/8vY87oA== + dependencies: + "@backstage/config" "^0.1.2" + "@backstage/core-api" "^0.2.8" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" @@ -1877,20 +1920,53 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/plugin-catalog@^0.2.1": - version "0.4.0" +"@backstage/plugin-catalog-react@^0.0.2": + version "0.0.2" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.2.tgz#e50da2dac9fab3a0d5973f8d1083ee2c368e5e52" + integrity sha512-O6aujFPRaEFTk4XlwOoswbnoHIOqMtj6ycUj6R1mNKOM4plUgGDKKhO3be69FHMJEMbiSvVe6AW+1kXaK+1LqA== + dependencies: + "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.0" + "@material-ui/core" "^4.11.0" + "@types/react" "^16.9" + react "^16.13.1" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + +"@backstage/plugin-catalog-react@^0.0.4": + version "0.0.4" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.4.tgz#a4c8ba90cf48106ac6af2e03afa6338010a1299b" + integrity sha512-1fAqULJvLyE+3SeZ2yxDJnJ3SbUFv2Im55d3KbMgRaSog1chSJJoO3jbIwIRQIBXjRmCXrZbf56qwwWwxj6OjA== dependencies: "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.2" - "@backstage/core" "^0.6.3" - "@backstage/plugin-catalog-react" "^0.1.0" - "@backstage/theme" "^0.2.3" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.2" + "@material-ui/core" "^4.11.0" + "@types/react" "^16.9" + react "^16.13.1" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + +"@backstage/plugin-catalog@^0.2.1": + version "0.2.14" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog/-/plugin-catalog-0.2.14.tgz#50a4176a55ffa543a426ec78cbc9deaecdbcf2b7" + integrity sha512-lDmNcC+m1zbbzYATUp5yIZ5PUp+YyBc1KKu3CCgqjLWSbJ1aJrU1N4g59euel1l2+qSW+lH76Kkp6ZYpZbSO9A== + dependencies: + "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-model" "^0.7.0" + "@backstage/core" "^0.5.0" + "@backstage/plugin-scaffolder" "^0.4.1" + "@backstage/theme" "^0.2.2" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" "@types/react" "^16.9" classnames "^2.2.6" git-url-parse "^11.4.4" + moment "^2.26.0" react "^16.13.1" react-dom "^16.13.1" react-helmet "6.1.0" @@ -1900,12 +1976,15 @@ swr "^0.3.0" "@backstage/plugin-catalog@^0.3.1": - version "0.4.0" + version "0.3.2" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog/-/plugin-catalog-0.3.2.tgz#06945f10fd678efdade3f2795590c12433568fa0" + integrity sha512-iHLxPHRN9nYIXwOEAQ06m+PagsFb6Nb/XjJSebCAnSrAxPPITvBCfxc2H1GbyyMdC7KAr1ozORB/FFkNsCaQJg== dependencies: "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.2" - "@backstage/core" "^0.6.3" - "@backstage/plugin-catalog-react" "^0.1.0" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.2" + "@backstage/plugin-catalog-react" "^0.0.4" + "@backstage/plugin-scaffolder" "^0.5.1" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -1946,6 +2025,56 @@ react-use "^15.3.3" swr "^0.3.0" +"@backstage/plugin-scaffolder@^0.4.1": + version "0.4.2" + resolved "https://registry.npmjs.org/@backstage/plugin-scaffolder/-/plugin-scaffolder-0.4.2.tgz#58159227997f7e248ce52535bc32f19fcd0990dc" + integrity sha512-YuyHM587Rqg6KufxfFqQdI7dsZniBM/11Aj8Q0m5ZszOpCuNmDDkR1VX8MKHTBJ709mnLAqRgArdla7FOrOAXQ== + dependencies: + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.0" + "@backstage/plugin-catalog-react" "^0.0.2" + "@backstage/theme" "^0.2.3" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@rjsf/core" "^2.4.0" + "@rjsf/material-ui" "^2.4.0" + classnames "^2.2.6" + git-url-parse "^11.4.4" + moment "^2.26.0" + react "^16.13.1" + react-dom "^16.13.1" + react-lazylog "^4.5.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + swr "^0.3.0" + +"@backstage/plugin-scaffolder@^0.5.1": + version "0.5.1" + resolved "https://registry.npmjs.org/@backstage/plugin-scaffolder/-/plugin-scaffolder-0.5.1.tgz#9d36f6b01991ddd9f9f2996068f3f31c766db8db" + integrity sha512-EG+iUc107bneVBPQpFKGp2jD9Y9+x50g/gY6TBN1je8TkgyluoxMj7wKv9e+d4TeGzXwK/LW/suajG9Zo0TJGQ== + dependencies: + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.2" + "@backstage/plugin-catalog-react" "^0.0.4" + "@backstage/theme" "^0.2.3" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@rjsf/core" "^2.4.0" + "@rjsf/material-ui" "^2.4.0" + classnames "^2.2.6" + git-url-parse "^11.4.4" + moment "^2.26.0" + react "^16.13.1" + react-dom "^16.13.1" + react-lazylog "^4.5.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + swr "^0.3.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -15076,6 +15205,11 @@ immer@1.10.0: resolved "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d" integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg== +immer@^7.0.9: + version "7.0.15" + resolved "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz#dc3bc6db87401659d2e737c67a21b227c484a4ad" + integrity sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA== + immer@^8.0.1: version "8.0.1" resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" @@ -18630,7 +18764,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.19.3, moment@^2.25.3, moment@^2.27.0: +moment@^2.19.3, moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -24025,6 +24159,11 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" +stream-mock@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/stream-mock/-/stream-mock-2.0.5.tgz#c99d24bd6dbb0eaa57cf6ffefdb064150747826e" + integrity sha512-dx9skT8QYjwLsal+MhGHr4UtgS49brw851C/oTixmhCi4Ip+/qnZmhV1qOcznYYAED6gYKmKea+jjza4/wjpSg== + stream-shift@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"