* 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:
jc21
2020-02-19 15:55:06 +11:00
committed by GitHub
parent bf036cbb88
commit bb0f4bfa62
517 changed files with 26256 additions and 11724 deletions

8
test/cypress/Dockerfile Normal file
View 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"]

View 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
}
}

View 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"
}
}

View 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"
}

View 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');
});
});
});

View 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;

View 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);
};

View 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);
}
};
};

View 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;
};

View 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);
});
});

View 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;
});