Version 3 starter

This commit is contained in:
Jamie Curnow
2021-06-14 19:29:35 +10:00
parent 60fc57431a
commit 6205434140
642 changed files with 25817 additions and 32319 deletions

View File

@@ -1,11 +1,6 @@
FROM cypress/included:5.6.0
COPY --chown=1000 ./ /test
# mkcert
ENV MKCERT=1.4.2
RUN wget -O /usr/bin/mkcert "https://github.com/FiloSottile/mkcert/releases/download/v${MKCERT}/mkcert-v${MKCERT}-linux-amd64" \
&& chmod +x /usr/bin/mkcert
COPY --chown=1000 ./test /test
WORKDIR /test
RUN yarn install

View File

@@ -0,0 +1,119 @@
/// <reference types="Cypress" />
describe('Certificates endpoints', () => {
let token;
let certID;
before(() => {
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to create new certificate', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/certificates',
data: {
type: 'http',
certificate_authority_id: 1,
name: 'My First Cert',
domain_names: [
'jc21.com',
'*.google.com'
]
}
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('post', 201, '/certificates', data);
expect(data.result).to.have.property('id');
expect(data.result.id).to.be.greaterThan(0);
expect(data.result.user_id).to.be.greaterThan(0);
certID = data.result.id;
});
});
it('Should be able to get a certificate', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/certificates/' + certID
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('get', 200, '/certificates/{certificateID}', data);
expect(data.result).to.have.property('id', certID);
});
});
it('Should be able to update a certificate', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/certificates/' + certID,
data: {
name: 'My Updated Cert'
}
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('put', 200, '/certificates/{certificateID}', data);
expect(data.result).to.have.property('id', certID);
expect(data.result).to.have.property('name', 'My Updated Cert');
});
});
it('Should be able to get all certificates', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/certificates'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/certificates', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.be.greaterThan(0);
});
});
it('Should be able to get all certificates with filters A', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/certificates?sort=name&name:starts=my&limit=1'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/certificates', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.be.greaterThan(0);
});
});
it('Should be able to get all certificates with filters B', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/certificates?id:in=1,2,3,4,5&limit=1'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/certificates', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.eq(1);
});
});
it('Should be able to get all certificates with filters C', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/certificates?name:starts=xxxxxxxxxxxxxxx'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/certificates', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('total', 0);
});
});
it('Should be able to delete a certificate', function() {
cy.task('backendApiDelete', {
token: token,
path: '/api/certificates/' + certID
}).then((data) => {
cy.validateSwaggerSchema('delete', 200, '/certificates/{certificateID}', data);
expect(data).to.have.property('result', true);
});
});
});

View File

@@ -1,20 +1,12 @@
/// <reference types="Cypress" />
describe('Basic API checks', () => {
it('Should return a valid health payload', function () {
it('Should return a valid health payload', function() {
cy.task('backendApiGet', {
path: '/api/',
}).then((data) => {
// Check the swagger schema:
expect(data.result.healthy, 'healthy should equal true').to.equal(true);
cy.validateSwaggerSchema('get', 200, '/', data);
});
});
it('Should return a valid schema payload', function () {
cy.task('backendApiGet', {
path: '/api/schema',
}).then((data) => {
expect(data.openapi).to.be.equal('3.0.0');
});
});
});

View File

@@ -0,0 +1,118 @@
/// <reference types="Cypress" />
const generateRandomString = function (length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
describe('Settings endpoints', () => {
let token;
let settingName = 'cypressSetting_' + generateRandomString(12);
before(() => {
cy.getToken().then((tok) => {
token = tok;
});
});
it('Should be able to create new settting', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/settings',
data: {
name: settingName,
value: {
type: 'custom',
html: '<p>not found</p>'
}
}
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('post', 201, '/settings', data);
expect(data.result).to.have.property('id');
expect(data.result.id).to.be.greaterThan(0);
});
});
it('Should be able to get a settting', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/settings/' + settingName
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('get', 200, '/settings/{name}', data);
expect(data.result).to.have.property('id');
expect(data.result).to.have.property('name', settingName);
expect(data.result.id).to.be.greaterThan(0);
});
});
it('Should be able to update a settting', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/settings/' + settingName,
data: {
value: true
}
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('put', 200, '/settings/{name}', data);
expect(data.result).to.have.property('id');
expect(data.result).to.have.property('name', settingName);
expect(data.result.id).to.be.greaterThan(0);
});
});
it('Should be able to get all settings', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/settings'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/settings', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.be.greaterThan(0);
});
});
it('Should be able to get all settings with filters A', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/settings?sort=name&name:starts=e&limit=1'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/settings', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.be.greaterThan(0);
});
});
it('Should be able to get all settings with filters B', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/settings?id:in=1,2,3,4,5&limit=1'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/settings', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.be.greaterThan(0);
});
});
it('Should be able to get all settings with filters C', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/settings?name:starts=xxxxxxxxxxxxxxx'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/settings', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('total', 0);
});
});
});

View File

@@ -0,0 +1,29 @@
/// <reference types="Cypress" />
describe('Setup Phase', () => {
before(() => {
cy.task('backendApiDelete', {
path: '/api/users'
}).then((data) => {
expect(data).to.have.property('result', true);
});
});
it('Should not be able to get a token', function() {
cy.task('backendApiPost', {
path: '/api/tokens',
data: {
type: 'password',
identity: 'jc@jc21.com',
secret: 'changeme'
},
returnOnError: true
}).then((data) => {
cy.validateSwaggerSchema('post', 403, '/tokens', data);
expect(data.error).to.have.property('code', 403);
expect(data.error).to.have.property('message', 'Not available during setup phase');
});
});
});

View File

@@ -0,0 +1,9 @@
/// <reference types="Cypress" />
describe('Swagger Schema Checks', () => {
it('Should be valid with swagger-validator', function() {
cy.task('validateSwaggerFile', {
file: Cypress.env('swaggerBase')
}).should('equal', null);
});
});

View File

@@ -1,7 +1,19 @@
/// <reference types="Cypress" />
const generateRandomString = function (length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
describe('Users endpoints', () => {
let token;
let uniqueEmail = 'jc_' + generateRandomString(10) + '@example.com';
let myUserID = 0;
before(() => {
cy.getToken().then((tok) => {
@@ -15,8 +27,10 @@ describe('Users endpoints', () => {
path: '/api/users/me'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users/{userID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data).to.have.property('result');
expect(data.result).to.have.property('id');
expect(data.result.id).to.be.greaterThan(0);
myUserID = data.result.id;
});
});
@@ -26,7 +40,91 @@ describe('Users endpoints', () => {
path: '/api/users'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users', data);
expect(data.length).to.be.greaterThan(0);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.be.greaterThan(0);
});
});
it('Should be able to get all users with filters A', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/users?sort=id.desc&name:starts=J&name:ends=w'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.be.greaterThan(0);
});
});
it('Should be able to get all users with filters B', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/users?sort=id&id:in=1,2,3,4,5'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('items');
expect(data.result.items.length).to.be.greaterThan(0);
});
});
it('Should be able to get all users with filters C', function() {
cy.task('backendApiGet', {
token: token,
path: '/api/users?sort=id&name:ends=xxxxxxxxxxxxx'
}).then((data) => {
cy.validateSwaggerSchema('get', 200, '/users', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('total', 0);
});
});
it('Should be able to create someone else', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/users',
data: {
name: 'Example user 1',
nickname: 'User1',
email: uniqueEmail,
roles: ['admin'],
is_disabled: false,
auth: {
type: 'password',
secret: 'changeme'
}
}
}).then((data) => {
cy.validateSwaggerSchema('post', 201, '/users', data);
expect(data).to.have.property('result');
expect(data.result).to.have.property('id');
expect(data.result.id).to.be.greaterThan(0);
});
});
it('Should not be able to create duplicate email', function() {
cy.task('backendApiPost', {
token: token,
path: '/api/users',
returnOnError: true,
data: {
name: 'Example user 2',
nickname: 'User2',
email: uniqueEmail,
roles: ['admin'],
is_disabled: false,
auth: {
type: 'password',
secret: 'changeme'
}
}
}).then((data) => {
cy.validateSwaggerSchema('post', 400, '/users', data);
expect(data).to.have.property('result', null);
expect(data).to.have.property('error');
expect(data.error).to.have.property('code', 400);
});
});
@@ -39,9 +137,55 @@ describe('Users endpoints', () => {
}
}).then((data) => {
cy.validateSwaggerSchema('put', 200, '/users/{userID}', data);
expect(data).to.have.property('id');
expect(data.id).to.be.greaterThan(0);
expect(data.name).to.be.equal('changed name');
expect(data).to.have.property('result');
expect(data.result).to.have.property('id');
expect(data.result.id).to.be.greaterThan(0);
expect(data.result.name).to.be.equal('changed name');
});
});
it('Should not be able to update email to another user', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/users/me',
returnOnError: true,
data: {
email: uniqueEmail
}
}).then((data) => {
cy.validateSwaggerSchema('put', 400, '/users/{userID}', data);
expect(data).to.have.property('result', null);
expect(data).to.have.property('error');
expect(data.error).to.have.property('code', 400);
});
});
it('Should not be able to disable yourself', function() {
cy.task('backendApiPut', {
token: token,
path: '/api/users/me',
returnOnError: true,
data: {
is_disabled: true
}
}).then((data) => {
cy.validateSwaggerSchema('put', 400, '/users/{userID}', data);
expect(data).to.have.property('result', null);
expect(data).to.have.property('error');
expect(data.error).to.have.property('code', 400);
});
});
it('Should not be able to delete yourself', function() {
cy.task('backendApiDelete', {
token: token,
path: '/api/users/' + myUserID,
returnOnError: true
}).then((data) => {
cy.validateSwaggerSchema('delete', 400, '/users/{userID}', data);
expect(data).to.have.property('result', null);
expect(data).to.have.property('error');
expect(data.error).to.have.property('code', 400);
});
});

View File

@@ -19,22 +19,17 @@ BackendApi.prototype.setToken = function(token) {
* @returns {Promise<object>}
*/
BackendApi.prototype.get = function(path, returnOnError) {
logger('GET Request:', this.config.baseUrl + path);
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,
headers: {
Accept: 'application/json',
Authorization: 'Bearer ' + this.token,
},
})
.on('complete', function(data, response) {
logger('Response data:', data);
logger('Response data:', JSON.stringify(data, null, 2));
if (!returnOnError && data instanceof Error) {
reject(data);
} else if (!returnOnError && response.statusCode != 200) {
@@ -56,22 +51,17 @@ BackendApi.prototype.get = function(path, returnOnError) {
* @returns {Promise<object>}
*/
BackendApi.prototype.delete = function(path, returnOnError) {
logger('DELETE Request:', this.config.baseUrl + path);
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,
headers: {
Accept: 'application/json',
Authorization: 'Bearer ' + this.token,
},
})
.on('complete', function(data, response) {
logger('Response data:', data);
logger('Response data:', JSON.stringify(data, null, 2));
if (!returnOnError && data instanceof Error) {
reject(data);
} else if (!returnOnError && response.statusCode != 200) {
@@ -94,7 +84,7 @@ BackendApi.prototype.delete = function(path, returnOnError) {
* @returns {Promise<object>}
*/
BackendApi.prototype.postJson = function(path, data, returnOnError) {
logger('POST ', this.config.baseUrl + path);
logger('POST Request:', this.config.baseUrl + path, JSON.stringify(data, null, 2));
return this._putPostJson('postJson', path, data, returnOnError);
};
@@ -105,7 +95,7 @@ BackendApi.prototype.postJson = function(path, data, returnOnError) {
* @returns {Promise<object>}
*/
BackendApi.prototype.putJson = function(path, data, returnOnError) {
logger('PUT ', this.config.baseUrl + path);
logger('PUT Request:', this.config.baseUrl + path, JSON.stringify(data, null, 2));
return this._putPostJson('putJson', path, data, returnOnError);
};
@@ -123,7 +113,7 @@ BackendApi.prototype._putPostJson = function(fn, path, data, returnOnError) {
Authorization: 'Bearer ' + this.token,
},
}).on('complete', function(data, response) {
logger('Response data:', data);
logger('Response data:', JSON.stringify(data, null, 2));
if (!returnOnError && data instanceof Error) {
reject(data);
} else if (!returnOnError && response.statusCode != 200) {

View File

@@ -9,8 +9,8 @@ module.exports = function (config) {
/**
* @param {object} options
* @param {string} options.token JWT
* @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}
*/

View File

@@ -1,7 +1,7 @@
const {SwaggerValidation} = require('@jc21/cypress-swagger-validation');
module.exports = (on, config) => {
// Replace swaggerBase config var wildcard
// Replace swaggerFile config var wildcard
if (typeof config.env.swaggerBase !== 'undefined') {
config.env.swaggerBase = config.env.swaggerBase.replace('{{baseUrl}}', config.baseUrl);
}

View File

@@ -13,30 +13,70 @@
* Check the swagger schema:
*
* @param {string} method API Method in swagger doc, "get", "put", "post", "delete"
* @param {number} statusCode API status code in swagger doc
* @param {integer} code Swagger doc endpoint response code, exactly as defined in swagger doc
* @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, statusCode, path, data) => {
Cypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => {
cy.task('validateSwaggerSchema', {
file: Cypress.env('swaggerBase'),
endpoint: path,
method: method,
statusCode: statusCode,
statusCode: code,
responseSchema: data,
verbose: true
}).should('equal', null);
});
Cypress.Commands.add('getToken', () => {
// login with existing user
cy.task('backendApiPost', {
path: '/api/tokens',
data: {
identity: 'admin@example.com',
secret: 'changeme'
cy.task('backendApiGet', {
path: '/api/',
}).then((data) => {
// Check the swagger schema:
cy.validateSwaggerSchema('get', 200, '/', data);
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);
});
}
}).then(res => {
cy.wrap(res.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.validateSwaggerSchema('post', 201, '/users', data);
expect(data.result).to.have.property('id');
expect(data.result.id).to.be.greaterThan(0);
cy.wrap(data.result);
});
});