From 3269a596004d20452925e86c0d2dcebe543537ca Mon Sep 17 00:00:00 2001 From: Yassine Date: Tue, 15 Jun 2021 11:52:04 -0400 Subject: [PATCH] feat: add light preview (#254) https://coveord.atlassian.net/browse/CDX-346 --- packages/cli/package-lock.json | 6 +- packages/cli/package.json | 4 +- .../src/commands/org/config/preview.spec.ts | 220 ++++++++++++++++++ .../cli/src/commands/org/config/preview.ts | 27 ++- .../cli/src/lib/snapshot/reportViewer.spec.ts | 201 ++++++++++++++++ packages/cli/src/lib/snapshot/reportViewer.ts | 188 +++++++++++++++ .../cli/src/lib/snapshot/snapshot.spec.ts | 37 +-- packages/cli/src/lib/snapshot/snapshot.ts | 10 +- 8 files changed, 658 insertions(+), 35 deletions(-) create mode 100644 packages/cli/src/commands/org/config/preview.spec.ts create mode 100644 packages/cli/src/lib/snapshot/reportViewer.spec.ts create mode 100644 packages/cli/src/lib/snapshot/reportViewer.ts diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 407e8d22e4..ec2db0119f 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1397,9 +1397,9 @@ "dev": true }, "@coveord/platform-client": { - "version": "19.14.0", - "resolved": "https://registry.npmjs.org/@coveord/platform-client/-/platform-client-19.14.0.tgz", - "integrity": "sha512-SXcY/bq1FCABv//4jU8YdKjcsSatIURoCpRsKylezUC/sHOzg0JAnTZCIOuM75bdNAMSZsLuR0YVyT08+nVLmQ==", + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@coveord/platform-client/-/platform-client-20.2.0.tgz", + "integrity": "sha512-j+qhEhZ8b6E/2tv3lziCbedDXMsY/w5SJyTY+QTfmJD3qdmoxsGPMdCQ71kp4g+efkAjdT6xSmp6BCrJXTYrJQ==", "requires": { "exponential-backoff": "^3.1.0", "form-data": "^3.0.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 138ca64c42..b959084179 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "@angular/cli": "^11.1.4", - "@coveord/platform-client": "^19.14.0", + "@coveord/platform-client": "^20.2.0", "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/plugin-help": "^3", @@ -16,6 +16,7 @@ "@vue/cli": "^4.5.11", "abortcontroller-polyfill": "^1.7.1", "archiver": "^5.3.0", + "chalk": "^4.1.1", "cli-ux": "^5.5.1", "coveo.analytics": "^2.18.4", "create-react-app": "^4.0.3", @@ -34,6 +35,7 @@ "@coveo/cra-template": "^1.3.0", "@coveo/vue-cli-plugin-typescript": "^1.3.0", "@oclif/dev-cli": "^1.26.0", + "@oclif/errors": "^1.3.4", "@oclif/test": "^1", "@types/archiver": "^5.1.0", "@types/cli-progress": "^3.9.1", diff --git a/packages/cli/src/commands/org/config/preview.spec.ts b/packages/cli/src/commands/org/config/preview.spec.ts new file mode 100644 index 0000000000..dfec36e0ab --- /dev/null +++ b/packages/cli/src/commands/org/config/preview.spec.ts @@ -0,0 +1,220 @@ +jest.mock('../../../lib/config/config'); +jest.mock('../../../hooks/analytics/analytics'); +jest.mock('../../../hooks/prerun/prerun'); +jest.mock('../../../lib/platform/authenticatedClient'); +jest.mock('../../../lib/snapshot/snapshot'); +jest.mock('../../../lib/snapshot/snapshotFactory'); +jest.mock('../../../lib/project/project'); +jest.mock('@oclif/errors'); + +import {mocked} from 'ts-jest/utils'; +import {test} from '@oclif/test'; +import {Project} from '../../../lib/project/project'; +import {join, normalize} from 'path'; +import {cwd} from 'process'; +import {Config} from '../../../lib/config/config'; +import {SnapshotFactory} from '../../../lib/snapshot/snapshotFactory'; +import {Snapshot} from '../../../lib/snapshot/snapshot'; +import {warn, error} from '@oclif/errors'; + +const mockedSnapshotFactory = mocked(SnapshotFactory, true); +const mockedConfig = mocked(Config); +const mockedProject = mocked(Project); +const mockedWarn = mocked(warn); +const mockedError = mocked(error); +const mockedConfigGet = jest.fn(); +const mockedDeleteTemporaryZipFile = jest.fn(); +const mockedDeleteSnapshot = jest.fn(); +const mockedSaveDetailedReport = jest.fn(); +const mockedRequiresSynchronization = jest.fn(); +const mockedPreviewSnapshot = jest.fn(); +const mockedLastReport = jest.fn(); + +const mockProject = () => { + mockedProject.mockImplementation( + () => + ({ + compressResources: () => + Promise.resolve(normalize(join('path', 'to', 'resources.zip'))), + deleteTemporaryZipFile: mockedDeleteTemporaryZipFile, + } as unknown as Project) + ); +}; + +const mockConfig = () => { + mockedConfigGet.mockReturnValue( + Promise.resolve({ + region: 'us-east-1', + organization: 'foo', + environment: 'prod', + }) + ); + + // TODO: use prototype + mockedConfig.mockImplementation( + () => + ({ + get: mockedConfigGet, + } as unknown as Config) + ); +}; + +const mockSnapshotFactory = async (validResponse: unknown) => { + mockedSnapshotFactory.createFromZip.mockReturnValue( + Promise.resolve({ + validate: () => Promise.resolve(validResponse), + preview: mockedPreviewSnapshot, + delete: mockedDeleteSnapshot, + saveDetailedReport: mockedSaveDetailedReport, + requiresSynchronization: mockedRequiresSynchronization, + latestReport: mockedLastReport, + id: 'banana-snapshot', + targetId: 'potato-org', + } as unknown as Snapshot) + ); +}; + +const mockSnapshotFactoryReturningValidSnapshot = async () => { + await mockSnapshotFactory({isValid: true, report: {}}); +}; + +const mockSnapshotFactoryReturningInvalidSnapshot = async () => { + await mockSnapshotFactory({isValid: false, report: {}}); +}; + +describe('org:config:preview', () => { + beforeAll(() => { + mockConfig(); + mockProject(); + }); + + describe('when the report contains no resources in error', () => { + beforeAll(async () => { + await mockSnapshotFactoryReturningValidSnapshot(); + }); + + afterAll(() => { + mockedSnapshotFactory.mockReset(); + }); + + test.command(['org:config:preview']).it('should use cwd as project', () => { + expect(mockedProject).toHaveBeenCalledWith(cwd()); + }); + + test + .command(['org:config:preview', '-p', 'path/to/project']) + .it('should use specifeid path for project', () => { + expect(mockedProject).toHaveBeenCalledWith( + normalize(join('path', 'to', 'project')) + ); + }); + + test + .command(['org:config:preview']) + .it('should work with default connected org', () => { + expect(mockedSnapshotFactory.createFromZip).toHaveBeenCalledWith( + normalize(join('path', 'to', 'resources.zip')), + 'foo' + ); + }); + + test + .command(['org:config:preview', '-t', 'myorg']) + .it('should work with specified target org', () => { + expect(mockedSnapshotFactory.createFromZip).toHaveBeenCalledWith( + normalize(join('path', 'to', 'resources.zip')), + 'myorg' + ); + }); + + test + .command(['org:config:preview']) + .it('should preview the snapshot', () => { + expect(mockedPreviewSnapshot).toHaveBeenCalledTimes(1); + }); + + test + .command(['org:config:preview']) + .it('should delete the compressed folder', () => { + expect(mockedDeleteTemporaryZipFile).toHaveBeenCalledTimes(1); + }); + + test + .command(['org:config:preview']) + .it('should delete the snapshot', () => { + expect(mockedDeleteSnapshot).toHaveBeenCalledTimes(1); + }); + }); + + describe('when the report contains resources in error', () => { + beforeAll(async () => { + await mockSnapshotFactoryReturningInvalidSnapshot(); + }); + + beforeEach(() => { + mockedRequiresSynchronization.mockReturnValueOnce(false); + mockedSaveDetailedReport.mockReturnValueOnce( + normalize(join('saved', 'snapshot')) + ); + }); + + afterAll(() => { + mockedSnapshotFactory.mockReset(); + }); + + test + .command(['org:config:preview']) + .it('should throw an error for invalid snapshots', () => { + expect(mockedError).toHaveBeenCalledWith( + expect.stringContaining('Invalid snapshot'), + {} + ); + }); + + test + .command(['org:config:preview']) + .it('should print an URL to the snapshot page', () => { + expect(mockedError).toHaveBeenCalledWith( + expect.stringContaining( + 'https://platform.cloud.coveo.com/admin/#potato-org/organization/resource-snapshots/banana-snapshot' + ), + {} + ); + }); + }); + + describe('when the snapshot is not in sync with the target org', () => { + beforeAll(async () => { + await mockSnapshotFactoryReturningInvalidSnapshot(); + }); + + beforeEach(() => { + mockedRequiresSynchronization.mockReturnValueOnce(true); + mockedSaveDetailedReport.mockReturnValueOnce(join('saved', 'snapshot')); + }); + + afterAll(() => { + mockedSnapshotFactory.mockReset(); + }); + + test + .command(['org:config:preview']) + .it('should have detected some conflicts', () => { + expect(mockedWarn).toHaveBeenCalledWith( + expect.stringContaining( + 'Some conflicts were detected while comparing changes between the snapshot and the target organization' + ) + ); + }); + + test + .command(['org:config:preview']) + .it('should print an url to the synchronization page', () => { + expect(mockedWarn).toHaveBeenCalledWith( + expect.stringContaining( + 'https://platform.cloud.coveo.com/admin/#potato-org/organization/resource-snapshots/banana-snapshot/synchronization' + ) + ); + }); + }); +}); diff --git a/packages/cli/src/commands/org/config/preview.ts b/packages/cli/src/commands/org/config/preview.ts index c32a3be847..829fd2b17d 100644 --- a/packages/cli/src/commands/org/config/preview.ts +++ b/packages/cli/src/commands/org/config/preview.ts @@ -12,6 +12,8 @@ import {Project} from '../../../lib/project/project'; import {SnapshotFactory} from '../../../lib/snapshot/snapshotFactory'; import {platformUrl} from '../../../lib/platform/environment'; import {Snapshot} from '../../../lib/snapshot/snapshot'; +import {red, green} from 'chalk'; +import {normalize} from 'path'; export interface CustomFile extends ReadStream { type?: string; @@ -42,7 +44,7 @@ export default class Preview extends Command { @Preconditions(IsAuthenticated()) public async run() { const {flags} = this.parse(Preview); - const project = new Project(flags.projectPath); + const project = new Project(normalize(flags.projectPath)); const pathToZip = await project.compressResources(); const targetOrg = await this.getTargetOrg(); @@ -55,15 +57,18 @@ export default class Preview extends Command { const {isValid} = await snapshot.validate(); if (!isValid) { - this.handleInvalidSnapshot(snapshot); - } else { - await snapshot.preview(); + await this.handleInvalidSnapshot(snapshot); + } + + cli.action.stop(isValid ? green('✔') : red.bold('!')); + + await snapshot.preview(); + + if (isValid) { await snapshot.delete(); } project.deleteTemporaryZipFile(); - - cli.action.stop(); } public async getTargetOrg() { @@ -82,20 +87,20 @@ export default class Preview extends Command { const report = snapshot.latestReport; if (snapshot.requiresSynchronization()) { - cli.action.start('Synchronization'); - const synchronizationPlanUrl = await this.getSynchronizationPage( snapshot ); this.warn( - dedent`Some conflicts were detected while comparing changes between the snapshot and the target organization. + dedent` + Some conflicts were detected while comparing changes between the snapshot and the target organization. Click on the URL below to synchronize your snapshot with your organization before running the command again. - ${synchronizationPlanUrl}` + ${synchronizationPlanUrl} + ` ); return; } - const snapshotUrl = this.getSnapshotPage(snapshot); + const snapshotUrl = await this.getSnapshotPage(snapshot); this.error( dedent`Invalid snapshot - ${report.resultCode}. diff --git a/packages/cli/src/lib/snapshot/reportViewer.spec.ts b/packages/cli/src/lib/snapshot/reportViewer.spec.ts new file mode 100644 index 0000000000..94ed061d18 --- /dev/null +++ b/packages/cli/src/lib/snapshot/reportViewer.spec.ts @@ -0,0 +1,201 @@ +import {test} from '@oclif/test'; + +import { + ResourceSnapshotsReportModel, + ResourceSnapshotsReportResultCode, + ResourceSnapshotsReportStatus, + ResourceSnapshotsReportType, +} from '@coveord/platform-client'; +import {ReportViewer} from './reportViewer'; +import dedent from 'ts-dedent'; + +const getReportWithoutChanges = ( + snapshotId: string +): ResourceSnapshotsReportModel => ({ + id: snapshotId, + updatedDate: 1622555847000, + type: ResourceSnapshotsReportType.DryRun, + status: ResourceSnapshotsReportStatus.Completed, + resourcesProcessed: 12, + resultCode: ResourceSnapshotsReportResultCode.Success, + resourceOperations: { + EXTENSION: { + resourcesCreated: 0, + resourcesUpdated: 0, + resourcesRecreated: 0, + resourcesDeleted: 0, + resourcesInError: 0, + resourcesUnchanged: 0, + }, + FIELD: { + resourcesCreated: 0, + resourcesUpdated: 0, + resourcesRecreated: 0, + resourcesDeleted: 0, + resourcesInError: 0, + resourcesUnchanged: 0, + }, + }, + resourceOperationResults: {}, +}); + +const getSuccessReport = ( + snapshotId: string +): ResourceSnapshotsReportModel => ({ + id: snapshotId, + updatedDate: 1622555847000, + resourcesProcessed: 99, + type: ResourceSnapshotsReportType.DryRun, + status: ResourceSnapshotsReportStatus.Completed, + resultCode: ResourceSnapshotsReportResultCode.Success, + resourceOperations: { + EXTENSION: { + resourcesCreated: 1, + resourcesUpdated: 0, + resourcesRecreated: 0, + resourcesDeleted: 2, + resourcesInError: 0, + resourcesUnchanged: 0, + }, + FIELD: { + resourcesCreated: 0, + resourcesUpdated: 1, + resourcesRecreated: 0, + resourcesDeleted: 0, + resourcesInError: 0, + resourcesUnchanged: 0, + }, + FILTER: { + resourcesCreated: 0, + resourcesUpdated: 0, + resourcesRecreated: 0, + resourcesDeleted: 0, + resourcesInError: 0, + resourcesUnchanged: 0, + }, + }, + resourceOperationResults: {}, +}); + +const getErrorReport = (snapshotId: string): ResourceSnapshotsReportModel => ({ + id: snapshotId, + updatedDate: 1622555847000, + type: ResourceSnapshotsReportType.DryRun, + status: ResourceSnapshotsReportStatus.Completed, + resultCode: ResourceSnapshotsReportResultCode.ResourcesInError, + resourcesProcessed: 99, + resourceOperations: { + EXTENSION: { + resourcesCreated: 1, + resourcesUpdated: 0, + resourcesRecreated: 0, + resourcesDeleted: 2, + resourcesInError: 0, + resourcesUnchanged: 0, + }, + FIELD: { + resourcesCreated: 0, + resourcesUpdated: 1, + resourcesRecreated: 0, + resourcesDeleted: 0, + resourcesInError: 7, + resourcesUnchanged: 0, + }, + }, + resourceOperationResults: { + FIELD: { + foo_4VNj5ds5: ['RESOURCE_ALREADY_EXISTS: Field foo already exists.'], + bar_4VNj5ds5: ['RESOURCE_ALREADY_EXISTS: Field bar already exists.'], + dsads_4VNj5ds5: ['RESOURCE_ALREADY_EXISTS: Field dsads already exists.'], + fdww_4VNj5ds5: ['RESOURCE_ALREADY_EXISTS: Field fdww already exists.'], + csad_4VNj5ds5: ['RESOURCE_ALREADY_EXISTS: Field csad already exists.'], + hjkd_4VNj5ds5: ['RESOURCE_ALREADY_EXISTS: Field hjkd already exists.'], + fdasf_4VNj5ds5: ['RESOURCE_ALREADY_EXISTS: Field fdasf already exists.'], + }, + }, +}); + +describe('ReportViewer', () => { + describe('when the report contains errors', () => { + let viewer: ReportViewer; + beforeAll(() => { + viewer = new ReportViewer(getErrorReport('some-id')); + }); + + test + .stdout() + .do(() => { + viewer.display(); + }) + .it( + 'should print a report section with the resources in error', + (ctx) => { + expect(ctx.stdout).toContain('Error Report:'); + expect(ctx.stdout).toContain('7 resources in error'); + } + ); + + test + .stdout() + .do(() => { + viewer.display(); + }) + .it('should not print more than 5 errors per resources', (ctx) => { + expect(ctx.stdout).toContain( + dedent` + Fields + • RESOURCE_ALREADY_EXISTS: Field foo already exists. + • RESOURCE_ALREADY_EXISTS: Field bar already exists. + • RESOURCE_ALREADY_EXISTS: Field dsads already exists. + • RESOURCE_ALREADY_EXISTS: Field fdww already exists. + • RESOURCE_ALREADY_EXISTS: Field csad already exists. + (2 more errors)` + ); + }); + }); + + describe('when the report does not contain errors', () => { + let viewer: ReportViewer; + beforeAll(() => { + viewer = new ReportViewer(getSuccessReport('some-id')); + }); + + test + .stdout() + .do(() => { + viewer.display(); + }) + .it('should print resource changes', (ctx) => { + // Remove padding added by cli-ux so we can test the text and not the padding on the line + const trimedStdout = ctx.stdout + .split('\n') + .map((s) => s.trimEnd()) + .join('\n'); + + expect(trimedStdout).toContain(dedent` + Previewing resource changes: + Extensions + + 1 to create + - 2 to delete + + Fields + ~ 1 to update`); + }); + }); + + describe('when the report contains no changes', () => { + let viewer: ReportViewer; + beforeAll(() => { + viewer = new ReportViewer(getReportWithoutChanges('some-id')); + }); + + test + .stdout() + .do(() => { + viewer.display(); + }) + .it('should show that no changes were detected', (ctx) => { + expect(ctx.stdout).toContain('No changes detected'); + }); + }); +}); diff --git a/packages/cli/src/lib/snapshot/reportViewer.ts b/packages/cli/src/lib/snapshot/reportViewer.ts new file mode 100644 index 0000000000..3ffd3e459a --- /dev/null +++ b/packages/cli/src/lib/snapshot/reportViewer.ts @@ -0,0 +1,188 @@ +import { + ResourceSnapshotsReportModel, + ResourceSnapshotsReportOperationModel, + ResourceSnapshotsReportResultCode, +} from '@coveord/platform-client'; +import {cli} from 'cli-ux'; +import {bgHex, green, yellow, red, bold, italic} from 'chalk'; + +export class ReportViewer { + public static maximumNumberOfErrorsToPrint = 5; + public static styles = { + green: (txt: string) => green(txt), + yellow: (txt: string) => yellow(txt), + red: (txt: string) => red(txt), + header: (txt: string) => bold.hex('#1CEBCF')(txt), + error: (txt: string) => bgHex('#F64D64').hex('#272C3A')(txt), + }; + + public constructor(private readonly report: ResourceSnapshotsReportModel) {} + + public display(): void { + this.printTable(); + + if (!this.isSuccessReport()) { + this.handleReportErrors(); + } + } + + private printTable() { + if (this.changedResources.length === 0) { + cli.log(ReportViewer.styles.header('\nNo changes detected')); + return; + } + + cli.table(this.changedResources, { + resourceName: { + header: ReportViewer.styles.header('\nPreviewing resource changes:'), + get: (row) => this.printTableSection(row), + }, + }); + } + + // TODO: Change logic once SRC-4448 is complete + private printTableSection(row: { + resourceName: string; + operations: ResourceSnapshotsReportOperationModel; + }) { + const resourceType = this.prettyPrintResourceName(row.resourceName); + let output = ` ${resourceType}\n`; + + if (row.operations.resourcesCreated > 0) { + output += `${ReportViewer.styles.green( + '+' + )} ${ReportViewer.styles.green( + `${row.operations.resourcesCreated} to create` + )}\n`; + } + if (row.operations.resourcesRecreated > 0) { + output += `${ReportViewer.styles.yellow( + '+-' + )} ${ReportViewer.styles.yellow( + `${row.operations.resourcesCreated} to replace` + )}\n`; + } + if (row.operations.resourcesUpdated > 0) { + output += `${ReportViewer.styles.yellow( + '~' + )} ${ReportViewer.styles.yellow( + `${row.operations.resourcesUpdated} to update` + )}\n`; + } + // TODO: CDX-361: Only show delete items if delete flag is set to true + if (row.operations.resourcesDeleted > 0) { + output += `${ReportViewer.styles.red('-')} ${ReportViewer.styles.red( + `${row.operations.resourcesDeleted} to delete` + )}\n`; + } + if (row.operations.resourcesInError > 0) { + output += `${ReportViewer.styles.error( + `! ${row.operations.resourcesInError} in error ` + )}\n`; + } + + return output; + } + + private get changedResources() { + type resourceEntries = [string, ResourceSnapshotsReportOperationModel]; + const resourceHasAtLeastOneOperation = ([ + _, + operations, + ]: resourceEntries) => { + return ( + operations.resourcesCreated + + operations.resourcesUpdated + + operations.resourcesRecreated + + // TODO: CDX-361: Only count delete items if delete flag is set to true + operations.resourcesDeleted + + operations.resourcesInError > + 0 + ); + }; + + const convertArrayToObject = ([ + resourceName, + operations, + ]: resourceEntries) => ({ + resourceName, + operations, + }); + + return Object.entries(this.report.resourceOperations) + .filter(resourceHasAtLeastOneOperation) + .map(convertArrayToObject); + } + + private prettyPrintResourceName(resourceName: string): string { + const capitalized = + resourceName.charAt(0) + resourceName.slice(1).toLowerCase() + 's'; + return capitalized.replace(/_/g, ' '); + } + + private isSuccessReport(): boolean { + return this.report.resultCode === ResourceSnapshotsReportResultCode.Success; + } + + private getOperationTypeTotalCount( + type: keyof ResourceSnapshotsReportOperationModel + ) { + const count = Object.values(this.report.resourceOperations).reduce( + (count: number, current: ResourceSnapshotsReportOperationModel) => + count + current[type], + 0 + ); + + return count; + } + + private handleReportErrors() { + const totalErrorCount = this.getOperationTypeTotalCount('resourcesInError'); + + cli.log(ReportViewer.styles.header('Error Report:')); + cli.log( + ReportViewer.styles.error( + ` ${totalErrorCount} resource${ + totalErrorCount > 1 ? 's' : '' + } in error ` + ) + ); + + for (const resourceType in this.report.resourceOperationResults) { + this.logResourceErrors(resourceType); + } + // TODO: CDX-362: handle other invalid snashot cases + } + + private logResourceErrors(resourceType: string) { + let remainingErrorsToPrint = ReportViewer.maximumNumberOfErrorsToPrint; + const operationResult = this.report.resourceOperationResults[resourceType]; + const operationResultErrors = Object.values(operationResult); + + if (operationResultErrors.length === 0) { + return; + } + + cli.log(`\n ${this.prettyPrintResourceName(resourceType)}`); + + const errors = operationResultErrors.reduce( + (acc, curr) => acc.concat(curr), + [] + ); + + for (let j = 0; j < errors.length && remainingErrorsToPrint > 0; j++) { + cli.log(red(` • ${errors[j]}`)); + remainingErrorsToPrint--; + } + + const unprintedErrors = + errors.length - ReportViewer.maximumNumberOfErrorsToPrint; + if (unprintedErrors > 0) { + cli.log( + italic( + ` (${unprintedErrors} more error${unprintedErrors > 1 ? 's' : ''})` + ) + ); + } + } +} diff --git a/packages/cli/src/lib/snapshot/snapshot.spec.ts b/packages/cli/src/lib/snapshot/snapshot.spec.ts index ab0fd940cf..16d056bd1d 100644 --- a/packages/cli/src/lib/snapshot/snapshot.spec.ts +++ b/packages/cli/src/lib/snapshot/snapshot.spec.ts @@ -10,14 +10,13 @@ import { ResourceSnapshotsReportType, } from '@coveord/platform-client'; import {writeJsonSync, ensureFileSync} from 'fs-extra'; -import {join} from 'path'; +import {join, normalize} from 'path'; import {mocked} from 'ts-jest/utils'; import {AuthenticatedClient} from '../platform/authenticatedClient'; -import {ISnapshotValidation, Snapshot} from './snapshot'; +import {Snapshot} from './snapshot'; const mockedAuthenticatedClient = mocked(AuthenticatedClient, true); const mockedEnsureFileSync = mocked(ensureFileSync); -const mockedWriteJsonSync = mocked(writeJsonSync); const mockedCreateSnapshotFromFile = jest.fn(); const mockedPushSnapshot = jest.fn(); const mockedDeleteSnapshot = jest.fn(); @@ -178,28 +177,32 @@ describe('Snapshot', () => { it('#latestReport should ensure the file exists', async () => { await snapshot.validate(); - snapshot.saveDetailedReport(join('path', 'to', 'report')); + snapshot.saveDetailedReport(normalize(join('path', 'to', 'report'))); expect(mockedEnsureFileSync).toHaveBeenCalledWith( - join( - 'path', - 'to', - 'report', - 'snapshot-reports', - 'target-org-snapshot-id.json' + normalize( + join( + 'path', + 'to', + 'report', + 'snapshot-reports', + 'target-org-snapshot-id.json' + ) ) ); }); it('#latestReport should save detailed report', async () => { await snapshot.validate(); - snapshot.saveDetailedReport(join('path', 'to', 'report')); + snapshot.saveDetailedReport(normalize(join('path', 'to', 'report'))); expect(writeJsonSync).toHaveBeenCalledWith( - join( - 'path', - 'to', - 'report', - 'snapshot-reports', - 'target-org-snapshot-id.json' + normalize( + join( + 'path', + 'to', + 'report', + 'snapshot-reports', + 'target-org-snapshot-id.json' + ) ), expect.objectContaining({ id: 'target-org-snapshot-id', diff --git a/packages/cli/src/lib/snapshot/snapshot.ts b/packages/cli/src/lib/snapshot/snapshot.ts index c6591be6ba..a8ceadadbb 100644 --- a/packages/cli/src/lib/snapshot/snapshot.ts +++ b/packages/cli/src/lib/snapshot/snapshot.ts @@ -7,6 +7,7 @@ import { } from '@coveord/platform-client'; import {cli} from 'cli-ux'; import {backOff} from 'exponential-backoff'; +import {ReportViewer} from './reportViewer'; import {ensureFileSync, writeJsonSync} from 'fs-extra'; import {join} from 'path'; @@ -32,7 +33,6 @@ export class Snapshot { } public async preview() { - // TODO: get detailed report this.displayLightPreview(); this.displayExpandedPreview(); } @@ -65,7 +65,10 @@ export class Snapshot { if (!Array.isArray(this.model.reports) || this.model.reports.length === 0) { throw new Error(`No detailed report found for the snapshot ${this.id}`); } - return this.model.reports.slice(-1)[0]; + const sortedReports = this.model.reports.sort( + (a, b) => b.updatedDate - a.updatedDate + ); + return sortedReports[0]; } public get id() { @@ -81,7 +84,8 @@ export class Snapshot { } private displayLightPreview() { - // TODO: CDX-346 Display light preview + const report = new ReportViewer(this.latestReport); + report.display(); } private displayExpandedPreview() {