Skip to content

Commit

Permalink
feat(schema-diff): compare two environments api map (#476)
Browse files Browse the repository at this point in the history
  • Loading branch information
Scra3 authored Feb 13, 2023
1 parent d5446c9 commit 170da5d
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ With the Development Workflow activated.
Manage Forest Admin schema.

- `schema:apply` apply the current schema of your repository to the specified environment (using your `.forestadmin-schema.json` file).
- `schema:diff` allow to compare two environment schemas.
- `schema:update` refresh your schema by generating files that do not currently exist.

## Docker
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"handlebars": "4.7.7",
"inquirer": "6.2.0",
"joi": "14.3.1",
"json-diff": "1.0.0",
"jsonapi-serializer": "3.6.2",
"jsonwebtoken": "8.5.1",
"jwt-decode": "2.2.0",
Expand Down
55 changes: 55 additions & 0 deletions src/commands/schema/diff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const EnvironmentManager = require('../../services/environment-manager');
const AbstractAuthenticatedCommand = require('../../abstract-authenticated-command');

class DiffCommand extends AbstractAuthenticatedCommand {
init(plan) {
super.init(plan);
const {
assertPresent,
chalk,
env,
environmentRenderer,
errorHandler,
} = this.context;
assertPresent({ chalk, env });
this.chalk = chalk;
this.env = env;
this.environmentRenderer = environmentRenderer;
this.errorHandler = errorHandler;
}

async runIfAuthenticated() {
const parsed = this.parse(DiffCommand);
const config = { ...this.env, ...parsed.flags, ...parsed.args };
const manager = new EnvironmentManager(config);

const { environmentIdFrom, environmentIdTo } = config;
try {
const [apimapFrom, apimapTo] = await Promise.all([
manager.getEnvironmentApimap(environmentIdFrom),
manager.getEnvironmentApimap(environmentIdTo),
]);

this.environmentRenderer.renderApimapDiff(apimapFrom, apimapTo);
} catch (error) {
this.logger.error(
`Cannot fetch the environments ${this.chalk.bold(environmentIdFrom)} and ${this.chalk.bold(environmentIdTo)}.`,
);
this.logger.error(manager.handleEnvironmentError(error));
}
}
}

DiffCommand.description = 'Allow to compare two environment schemas';

DiffCommand.flags = {
help: AbstractAuthenticatedCommand.flags.boolean({
description: 'Display usage information.',
}),
};

DiffCommand.args = [
{ name: 'environmentIdFrom', required: true, description: 'ID of an environment to compare.' },
{ name: 'environmentIdTo', required: true, description: 'ID of an environment to compare.' },
];
module.exports = DiffCommand;
3 changes: 2 additions & 1 deletion src/context/dependencies-plan.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ module.exports = (plan) => plan
.addModule('joi', () => require('joi'))
.addModule('openIdClient', () => require('openid-client'))
.addModule('os', () => require('os'))
.addModule('superagent', () => require('superagent')));
.addModule('superagent', () => require('superagent'))
.addUsingFunction('diffString', () => require('json-diff').diffString));
13 changes: 13 additions & 0 deletions src/renderers/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ class EnvironmentRenderer {
chalk,
logger,
Table,
diffString,
}) {
assertPresent({
chalk,
logger,
Table,
diffString,
});
this.chalk = chalk;
this.logger = logger;
this.Table = Table;
this.diffString = diffString;
}

render(environment, config) {
Expand Down Expand Up @@ -46,6 +49,16 @@ class EnvironmentRenderer {
default:
}
}

renderApimapDiff(apimapFrom, apimapTo) {
const diff = this.diffString(apimapFrom, apimapTo);
if (diff) {
this.logger.log(this.chalk.bold.yellow('⚠ The schemas have differences.'));
this.logger.log(diff);
} else {
this.logger.log(this.chalk.bold.green('√ The schemas are identical.'));
}
}
}

module.exports = EnvironmentRenderer;
20 changes: 20 additions & 0 deletions src/services/environment-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const EnvironmentSerializer = require('../serializers/environment');
const environmentDeserializer = require('../deserializers/environment');
const DeploymentRequestSerializer = require('../serializers/deployment-request');
const JobStateChecker = require('./job-state-checker');
const { handleError } = require('../utils/error');

function EnvironmentManager(config) {
const {
Expand Down Expand Up @@ -35,6 +36,17 @@ function EnvironmentManager(config) {
.then((response) => environmentDeserializer.deserialize(response.body));
};

this.getEnvironmentApimap = async (environmentId) => {
const authToken = authenticator.getAuthToken();

const response = await agent
.get(`${env.FOREST_URL}/api/apimaps/${environmentId}`)
.set('Authorization', `Bearer ${authToken}`)
.set('forest-environment-id', environmentId)
.send();
return response.body.data.apimap;
};

this.createEnvironment = async () => {
const authToken = authenticator.getAuthToken();

Expand Down Expand Up @@ -134,6 +146,14 @@ function EnvironmentManager(config) {
.set('Authorization', `Bearer ${authToken}`)
.set('forest-secret-key', `${config.envSecret}`);
};

this.handleEnvironmentError = (rawError) => {
const error = handleError(rawError);
if (error === 'Forbidden') {
return 'You do not have the permission to perform this action on the given environments.';
}
return error;
};
}

module.exports = EnvironmentManager;
1 change: 1 addition & 0 deletions src/utils/terminator.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = ({
if (logs.length) {
logger.error(...logs);
}

if (errorCode) {
await eventSender.notifyError(errorCode, errorMessage, context);
} else {
Expand Down
85 changes: 85 additions & 0 deletions test/commands/schema/diff.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const jsonDiff = require('json-diff');
const testCli = require('../test-cli-helper/test-cli');
const DiffSchemaCommand = require('../../../src/commands/schema/diff');
const { testEnvWithSecret } = require('../../fixtures/env');
const {
loginValidOidc, getEnvironmentApimap,
getEnvironmentApimapForbidden,
} = require('../../fixtures/api');

describe('schema:diff', () => {
describe('when the user is not logged in', () => {
it('should login the user and does the diff', () => testCli({
env: testEnvWithSecret,
api: [
() => loginValidOidc(),
() => getEnvironmentApimap(10),
() => getEnvironmentApimap(11),
],
commandClass: DiffSchemaCommand,
commandArgs: ['10', '11'],
std: [
{ out: '> Login required.' },
{ out: 'Click on "Log in" on the browser tab which opened automatically or open this link: http://app.localhost/device/check?code=ABCD' },
{ out: 'Your confirmation code: USER-CODE' },
{ out: '> Login successful' },
{ out: '√ The schemas are identical.' },
],
}));
});

describe('when the user is logged in', () => {
describe('when schemas are identical', () => {
it('display "identical" message', () => testCli({
env: testEnvWithSecret,
token: 'any',
api: [
() => getEnvironmentApimap(10),
() => getEnvironmentApimap(11),
],
commandClass: DiffSchemaCommand,
commandArgs: ['10', '11'],
std: [
{ out: '√ The schemas are identical.' },
],
}));
});

describe('when schemas are not identical', () => {
const apiMapA = { collections: [{ name: 'Users' }] };
const apiMapB = { collections: [{ name: 'Users' }, { name: 'Posts' }] };

it('display the diff message', () => testCli({
env: testEnvWithSecret,
token: 'any',
api: [
() => getEnvironmentApimap(10, apiMapA),
() => getEnvironmentApimap(11, apiMapB),
],
commandClass: DiffSchemaCommand,
commandArgs: ['10', '11'],
std: [
{ out: '⚠ The schemas have differences.' },
{ in: jsonDiff.diffString(apiMapA, apiMapB) },
],
}));
});

describe('when there is an error', () => {
it('should display an error message', () => testCli({
env: testEnvWithSecret,
token: 'any',
api: [
() => getEnvironmentApimap(10),
() => getEnvironmentApimapForbidden(99999),
],
commandClass: DiffSchemaCommand,
commandArgs: ['10', '99999'],
std: [
{ err: '× Cannot fetch the environments 10 and 99999.' },
{ err: '× Oops something went wrong.' },
],
}));
});
});
});
10 changes: 10 additions & 0 deletions test/fixtures/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,16 @@ module.exports = {
.matchHeader('forest-environment-id', id)
.get(`/api/environments/${id}`)
.reply(404),
getEnvironmentApimap: (id, apimap = { collections: [] }) => nock('http://localhost:3001')
.matchHeader('forest-environment-id', id)
.get(`/api/apimaps/${id}`)
.reply(200, {
data: { apimap },
}),
getEnvironmentApimapForbidden: (id) => nock('http://localhost:3001')
.matchHeader('forest-environment-id', id)
.get(`/api/apimaps/${id}`)
.reply(403),

updateEnvironmentName: () => nock('http://localhost:3001')
.put('/api/environments/182', {
Expand Down
31 changes: 31 additions & 0 deletions test/services/environment-manager.unit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const Context = require('@forestadmin/context');
const EnvironmentManager = require('../../src/services/environment-manager');
const defaultPlan = require('../../src/context/plan');

describe('services > EnvironmentManager', () => {
const buildManager = () => {
// we must init the context for enable the dependency injection
Context.init(defaultPlan);

return new EnvironmentManager({});
};
describe('handleEnvironmentError', () => {
describe('when the error is unknown', () => {
it('should return the receives error', () => {
expect.assertions(1);
const error = new Error('error');
const result = buildManager().handleEnvironmentError(error);
expect(result).toBe('error');
});
});

describe('when the error is a 403 Forbidden', () => {
it('should return a forbidden error', () => {
expect.assertions(1);
const error = new Error('Forbidden');
const result = buildManager().handleEnvironmentError(error);
expect(result).toBe('You do not have the permission to perform this action on the given environments.');
});
});
});
});
Loading

0 comments on commit 170da5d

Please sign in to comment.