v2.1.0 (#293)
* Fix wrapping when too many hosts are shown (#207) * Update npm packages, fixes CVE-2019-10757 * Revert some breaking packages * Major overhaul - Docker buildx support in CI - Cypress API Testing in CI - Restructured folder layout (insert clean face meme) - Added Swagger documentation and validate API against that (to be completed) - Use common base image for all supported archs, which includes updated nginx with ipv6 support - Updated certbot and changes required for it - Large amount of Hosts names will wrap in UI - Updated packages for frontend - Version bump 2.1.0 * Updated documentation * Fix JWT expire time going crazy. Now set to 1day * Backend JS formatting rules * Remove v1 importer, I doubt anyone is using v1 anymore * Added backend formatting rules and enforce them in Jenkins builds * Fix CI, doesn't need a tty * Thanks bcrypt. Why can't you just be normal. * Cleanup after syntax check Co-authored-by: Marcelo Castagna <margaale@users.noreply.github.com>
This commit is contained in:
8
test/cypress/Dockerfile
Normal file
8
test/cypress/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
FROM cypress/included:3.8.3
|
||||
|
||||
COPY --chown=1000 ./test /test
|
||||
|
||||
WORKDIR /test
|
||||
RUN yarn install
|
||||
ENTRYPOINT []
|
||||
CMD ["cypress", "run"]
|
17
test/cypress/config/ci.json
Normal file
17
test/cypress/config/ci.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"requestTimeout": 30000,
|
||||
"defaultCommandTimeout": 20000,
|
||||
"reporter": "mocha-junit-reporter",
|
||||
"reporterOptions": {
|
||||
"jenkinsMode": true,
|
||||
"rootSuiteTitle": "Cypress",
|
||||
"jenkinsClassnamePrefix": "Cypress.",
|
||||
"mochaFile": "/results/junit/my-test-output-[hash].xml"
|
||||
},
|
||||
"videosFolder": "/results/videos",
|
||||
"screenshotsFolder": "/results/screenshots",
|
||||
"env": {
|
||||
"swaggerBase": "{{baseUrl}}/api/schema",
|
||||
"RETRIES": 4
|
||||
}
|
||||
}
|
13
test/cypress/config/dev.json
Normal file
13
test/cypress/config/dev.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"requestTimeout": 30000,
|
||||
"defaultCommandTimeout": 20000,
|
||||
"reporter": "junit",
|
||||
"reporterOptions": {
|
||||
"mochaFile": "results/junit/my-test-output-[hash].xml"
|
||||
},
|
||||
"video": false,
|
||||
"screenshotsFolder": "cypress/results/screenshots",
|
||||
"env": {
|
||||
"swaggerBase": "{{baseUrl}}/api/schema"
|
||||
}
|
||||
}
|
5
test/cypress/fixtures/example.json
Normal file
5
test/cypress/fixtures/example.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
22
test/cypress/integration/Health.spec.js
Normal file
22
test/cypress/integration/Health.spec.js
Normal file
@ -0,0 +1,22 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
describe('Basic API checks', () => {
|
||||
it('Should return a valid health payload', function () {
|
||||
cy.wait(2000);
|
||||
cy.task('backendApiGet', {
|
||||
path: '/api/',
|
||||
}).then((data) => {
|
||||
// Check the swagger schema:
|
||||
cy.validateSwaggerSchema('get', '/', data);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return a valid schema payload', function () {
|
||||
cy.wait(2000);
|
||||
cy.task('backendApiGet', {
|
||||
path: '/api/schema',
|
||||
}).then((data) => {
|
||||
expect(data.openapi).to.be.equal('3.0.0');
|
||||
});
|
||||
});
|
||||
});
|
142
test/cypress/plugins/backendApi/client.js
Normal file
142
test/cypress/plugins/backendApi/client.js
Normal file
@ -0,0 +1,142 @@
|
||||
const logger = require('./logger');
|
||||
const restler = require('@jc21/restler');
|
||||
|
||||
const BackendApi = function(config, token) {
|
||||
this.config = config;
|
||||
this.token = token;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} token
|
||||
*/
|
||||
BackendApi.prototype.setToken = function(token) {
|
||||
this.token = token;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {bool} [returnOnError]
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
BackendApi.prototype.get = function(path, returnOnError) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let headers = {
|
||||
Accept: 'application/json'
|
||||
};
|
||||
if (this.token) {
|
||||
headers.Authorization = 'Bearer ' + this.token;
|
||||
}
|
||||
|
||||
logger('GET ', this.config.baseUrl + path);
|
||||
|
||||
restler
|
||||
.get(this.config.baseUrl + path, {
|
||||
headers: headers,
|
||||
})
|
||||
.on('complete', function(data, response) {
|
||||
logger('Response data:', data);
|
||||
if (!returnOnError && data instanceof Error) {
|
||||
reject(data);
|
||||
} else if (!returnOnError && response.statusCode != 200) {
|
||||
if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
|
||||
reject(new Error(data.error.code + ': ' + data.error.message));
|
||||
} else {
|
||||
reject(new Error('Error ' + response.statusCode));
|
||||
}
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {bool} [returnOnError]
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
BackendApi.prototype.delete = function(path, returnOnError) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let headers = {
|
||||
Accept: 'application/json'
|
||||
};
|
||||
if (this.token) {
|
||||
headers.Authorization = 'Bearer ' + this.token;
|
||||
}
|
||||
|
||||
logger('DELETE ', this.config.baseUrl + path);
|
||||
|
||||
restler
|
||||
.del(this.config.baseUrl + path, {
|
||||
headers: headers,
|
||||
})
|
||||
.on('complete', function(data, response) {
|
||||
logger('Response data:', data);
|
||||
if (!returnOnError && data instanceof Error) {
|
||||
reject(data);
|
||||
} else if (!returnOnError && response.statusCode != 200) {
|
||||
if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
|
||||
reject(new Error(data.error.code + ': ' + data.error.message));
|
||||
} else {
|
||||
reject(new Error('Error ' + response.statusCode));
|
||||
}
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {object} data
|
||||
* @param {bool} [returnOnError]
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
BackendApi.prototype.postJson = function(path, data, returnOnError) {
|
||||
logger('POST ', this.config.baseUrl + path);
|
||||
return this._putPostJson('postJson', path, data, returnOnError);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {object} data
|
||||
* @param {bool} [returnOnError]
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
BackendApi.prototype.putJson = function(path, data, returnOnError) {
|
||||
logger('PUT ', this.config.baseUrl + path);
|
||||
return this._putPostJson('putJson', path, data, returnOnError);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {object} data
|
||||
* @param {bool} [returnOnError]
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
BackendApi.prototype._putPostJson = function(fn, path, data, returnOnError) {
|
||||
return new Promise((resolve, reject) => {
|
||||
restler[fn](this.config.baseUrl + path, data, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Authorization: 'Bearer ' + this.token,
|
||||
},
|
||||
}).on('complete', function(data, response) {
|
||||
logger('Response data:', data);
|
||||
if (!returnOnError && data instanceof Error) {
|
||||
reject(data);
|
||||
} else if (!returnOnError && response.statusCode != 200) {
|
||||
if (typeof data === 'object' && typeof data.error === 'object' && typeof data.error.message !== 'undefined') {
|
||||
reject(new Error(data.error.code + ': ' + data.error.message));
|
||||
} else {
|
||||
reject(new Error('Error ' + response.statusCode));
|
||||
}
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = BackendApi;
|
8
test/cypress/plugins/backendApi/logger.js
Normal file
8
test/cypress/plugins/backendApi/logger.js
Normal file
@ -0,0 +1,8 @@
|
||||
const _ = require('lodash');
|
||||
const chalk = require('chalk');
|
||||
|
||||
module.exports = function () {
|
||||
var arr = _.values(arguments);
|
||||
arr.unshift(chalk.blue.bold('[') + chalk.yellow.bold('Backend API') + chalk.blue.bold(']'));
|
||||
console.log.apply(null, arr);
|
||||
};
|
64
test/cypress/plugins/backendApi/task.js
Normal file
64
test/cypress/plugins/backendApi/task.js
Normal file
@ -0,0 +1,64 @@
|
||||
const logger = require('./logger');
|
||||
const Client = require('./client');
|
||||
|
||||
module.exports = function (config) {
|
||||
|
||||
logger('Client Ready using', config.baseUrl);
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.path API path
|
||||
* @param {string} [options.token] JWT
|
||||
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
|
||||
* @returns {string}
|
||||
*/
|
||||
backendApiGet: (options) => {
|
||||
const api = new Client(config);
|
||||
api.setToken(options.token);
|
||||
return api.get(options.path, options.returnOnError || false);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.token JWT
|
||||
* @param {string} options.path API path
|
||||
* @param {object} options.data
|
||||
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
|
||||
* @returns {string}
|
||||
*/
|
||||
backendApiPost: (options) => {
|
||||
const api = new Client(config);
|
||||
api.setToken(options.token);
|
||||
return api.postJson(options.path, options.data, options.returnOnError || false);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.token JWT
|
||||
* @param {string} options.path API path
|
||||
* @param {object} options.data
|
||||
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
|
||||
* @returns {string}
|
||||
*/
|
||||
backendApiPut: (options) => {
|
||||
const api = new Client(config);
|
||||
api.setToken(options.token);
|
||||
return api.putJson(options.path, options.data, options.returnOnError || false);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} options.token JWT
|
||||
* @param {string} options.path API path
|
||||
* @param {bool} [options.returnOnError] If true, will return instead of throwing errors
|
||||
* @returns {string}
|
||||
*/
|
||||
backendApiDelete: (options) => {
|
||||
const api = new Client(config);
|
||||
api.setToken(options.token);
|
||||
return api.delete(options.path, options.returnOnError || false);
|
||||
}
|
||||
};
|
||||
};
|
20
test/cypress/plugins/index.js
Normal file
20
test/cypress/plugins/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
const {SwaggerValidation} = require('@jc21/cypress-swagger-validation');
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// Replace swaggerBase config var wildcard
|
||||
if (typeof config.env.swaggerBase !== 'undefined') {
|
||||
config.env.swaggerBase = config.env.swaggerBase.replace('{{baseUrl}}', config.baseUrl);
|
||||
}
|
||||
|
||||
// Plugin Events
|
||||
on('task', SwaggerValidation(config));
|
||||
on('task', require('./backendApi/task')(config));
|
||||
on('task', {
|
||||
log(message) {
|
||||
console.log(message);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
};
|
94
test/cypress/support/commands.js
Normal file
94
test/cypress/support/commands.js
Normal file
@ -0,0 +1,94 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
|
||||
/**
|
||||
* Check the swagger schema:
|
||||
*
|
||||
* @param {string} method API Method in swagger doc, "get", "put", "post", "delete"
|
||||
* @param {string} path Swagger doc endpoint path, exactly as defined in swagger doc
|
||||
* @param {*} data The API response data to check against the swagger schema
|
||||
*/
|
||||
Cypress.Commands.add('validateSwaggerSchema', (method, path, data) => {
|
||||
cy.task('validateSwaggerSchema', {
|
||||
file: Cypress.env('swaggerBase'),
|
||||
endpoint: path,
|
||||
method: method,
|
||||
statusCode: 200,
|
||||
responseSchema: data,
|
||||
verbose: true
|
||||
}).should('equal', null);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getToken', () => {
|
||||
cy.task('backendApiGet', {
|
||||
path: '/api/',
|
||||
}).then((data) => {
|
||||
// Check the swagger schema:
|
||||
cy.task('validateSwaggerSchema', {
|
||||
endpoint: '/',
|
||||
method: 'get',
|
||||
statusCode: 200,
|
||||
responseSchema: data,
|
||||
verbose: true,
|
||||
}).should('equal', null);
|
||||
|
||||
if (!data.result.setup) {
|
||||
cy.log('Setup = false');
|
||||
// create a new user
|
||||
cy.createInitialUser().then(() => {
|
||||
return cy.getToken();
|
||||
});
|
||||
} else {
|
||||
cy.log('Setup = true');
|
||||
// login with existing user
|
||||
cy.task('backendApiPost', {
|
||||
path: '/api/tokens',
|
||||
data: {
|
||||
type: 'password',
|
||||
identity: 'jc@jc21.com',
|
||||
secret: 'changeme'
|
||||
}
|
||||
}).then(res => {
|
||||
cy.wrap(res.result.token);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('createInitialUser', () => {
|
||||
return cy.task('backendApiPost', {
|
||||
path: '/api/users',
|
||||
data: {
|
||||
name: 'Jamie Curnow',
|
||||
nickname: 'James',
|
||||
email: 'jc@jc21.com',
|
||||
roles: [],
|
||||
is_disabled: false,
|
||||
auth: {
|
||||
type: 'password',
|
||||
secret: 'changeme'
|
||||
}
|
||||
}
|
||||
}).then((data) => {
|
||||
// Check the swagger schema:
|
||||
cy.task('validateSwaggerSchema', {
|
||||
endpoint: '/users',
|
||||
method: 'post',
|
||||
statusCode: 201,
|
||||
responseSchema: data,
|
||||
verbose: true
|
||||
}).should('equal', null);
|
||||
|
||||
expect(data.result).to.have.property('id');
|
||||
expect(data.result.id).to.be.greaterThan(0);
|
||||
cy.wrap(data.result);
|
||||
});
|
||||
});
|
9
test/cypress/support/index.js
Normal file
9
test/cypress/support/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
require('cypress-plugin-retries');
|
||||
|
||||
import './commands';
|
||||
|
||||
Cypress.on('uncaught:exception', (/*err, runnable*/) => {
|
||||
// returning false here prevents Cypress from
|
||||
// failing the test
|
||||
return false;
|
||||
});
|
Reference in New Issue
Block a user