diff --git a/test/install.spec.ts b/test/install.spec.ts new file mode 100644 index 00000000..6cbf647a --- /dev/null +++ b/test/install.spec.ts @@ -0,0 +1,40 @@ +import * as path from 'path'; +import * as express from 'express'; +import { expect } from 'chai'; +import { OpenApiValidator } from '../src'; + +describe('install', () => { + it('should succeed when spec exists and is valid', async () => { + const apiSpec = path.join('test', 'resources', 'openapi.yaml'); + const oam = new OpenApiValidator({ apiSpec }); + + expect(oam) + .to.have.property('install') + .that.is.a('function'); + }); + + it('should throw when spec is missing', async () => { + try { + await new OpenApiValidator({ + apiSpec: './not-found.yaml', + }).install(express()); + } catch (e) { + expect(e.message).to.contain( + 'spec could not be read at ./not-found.yaml', + ); + } + }); + + it('should throw when security handlers are specified in new and old', async () => { + const apiSpec = path.join('test', 'resources', 'openapi.yaml'); + expect(function() { + return new OpenApiValidator({ + apiSpec, + validateSecurity: {}, + securityHandlers: {}, + }); + }).to.throw( + 'securityHandlers and validateSecurity may not be used together. Use validateSecurities.handlers to specify handlers.', + ); + }); +}); diff --git a/test/security.defaults.spec.ts b/test/security.defaults.spec.ts new file mode 100644 index 00000000..d31b44fb --- /dev/null +++ b/test/security.defaults.spec.ts @@ -0,0 +1,56 @@ +import * as path from 'path'; +import * as express from 'express'; +import { expect } from 'chai'; +import * as request from 'supertest'; +import { createApp } from './common/app'; + +describe('security.defaults', () => { + let app = null; + let basePath = null; + + before(async () => { + const apiSpec = path.join('test', 'resources', 'security.yaml'); + app = await createApp({ apiSpec }, 3005); + basePath = app.basePath; + + app.use( + `${basePath}`, + express + .Router() + .get(`/api_key`, (req, res) => res.json({ logged_in: true })) + .get(`/bearer`, (req, res) => res.json({ logged_in: true })) + .get(`/basic`, (req, res) => res.json({ logged_in: true })) + .get('/no_security', (req, res) => res.json({ logged_in: true })), + ); + }); + + after(() => { + app.server.close(); + }); + + it('should return 200 if no security', async () => + request(app) + .get(`${basePath}/no_security`) + .expect(200)); + + it('should skip validation, even if auth header is missing for basic auth', async () => { + return request(app) + .get(`${basePath}/basic`) + .expect(401) + .then(r => { + expect(r.body) + .to.have.property('message') + .that.equals('Authorization header required'); + }); + }); + + it('should skip security validation, even if auth header is missing for bearer auth', async () => { + return request(app) + .get(`${basePath}/bearer`) + .expect(401).then(r => { + expect(r.body) + .to.have.property('message') + .that.equals('Authorization header required'); + }) + }); +}); diff --git a/test/security.disabled.spec.ts b/test/security.disabled.spec.ts new file mode 100644 index 00000000..610c0bf3 --- /dev/null +++ b/test/security.disabled.spec.ts @@ -0,0 +1,47 @@ +import * as path from 'path'; +import * as express from 'express'; +import * as request from 'supertest'; +import { createApp } from './common/app'; + +// NOTE/TODO: These tests modify eovConf.validateSecurity.handlers +// Thus test execution order matters :-( +describe('security.disabled', () => { + let app = null; + let basePath = null; + before(async () => { + // Set up the express app + const apiSpec = path.join('test', 'resources', 'security.yaml'); + app = await createApp({ apiSpec, validateSecurity: false }, 3005); + basePath = app.basePath; + app.use( + `${basePath}`, + express + .Router() + .get(`/api_key`, (req, res) => res.json({ logged_in: true })) + .get(`/bearer`, (req, res) => res.json({ logged_in: true })) + .get(`/basic`, (req, res) => res.json({ logged_in: true })) + .get('/no_security', (req, res) => res.json({ logged_in: true })), + ); + }); + + after(() => { + app.server.close(); + }); + + it('should return 200 if no security', async () => + request(app) + .get(`${basePath}/no_security`) + .expect(200)); + + it('should skip validation, even if auth header is missing for basic auth', async () => { + return request(app) + .get(`${basePath}/basic`) + .expect(200); + }); + + it('should skip security validation, even if auth header is missing for bearer auth', async () => { + return request(app) + .get(`${basePath}/bearer`) + .expect(200); + }); +}); diff --git a/test/security.handlers.spec.ts b/test/security.handlers.spec.ts new file mode 100644 index 00000000..a51e675b --- /dev/null +++ b/test/security.handlers.spec.ts @@ -0,0 +1,389 @@ +import * as path from 'path'; +import * as express from 'express'; +import { expect } from 'chai'; +import * as request from 'supertest'; +import { createApp } from './common/app'; +import { + OpenApiValidatorOpts, + ValidateSecurityOpts, + OpenAPIV3, +} from '../src/framework/types'; + +// NOTE/TODO: These tests modify eovConf.validateSecurity.handlers +// Thus test execution order matters :-( +describe('security.handlers', () => { + let app = null; + let basePath = null; + const eovConf: OpenApiValidatorOpts = { + apiSpec: path.join('test', 'resources', 'security.yaml'), + validateSecurity: { + handlers: { + ApiKeyAuth: (req, scopes, schema) => { + throw Error('custom api key handler failed'); + }, + }, + }, + }; + before(async () => { + // Set up the express app + app = await createApp(eovConf, 3005); + basePath = app.basePath; + + app.use( + `${basePath}`, + express + .Router() + .get(`/api_key`, (req, res) => res.json({ logged_in: true })) + .get(`/bearer`, (req, res) => res.json({ logged_in: true })) + .get(`/basic`, (req, res) => res.json({ logged_in: true })) + .get(`/oauth2`, (req, res) => res.json({ logged_in: true })) + .get(`/openid`, (req, res) => res.json({ logged_in: true })) + .get(`/api_key_or_anonymous`, (req, res) => + res.json({ logged_in: true }), + ) + .get('/no_security', (req, res) => res.json({ logged_in: true })), + ); + }); + + after(() => { + app.server.close(); + }); + + it('should return 200 if no security', async () => + request(app) + .get(`${basePath}/no_security`) + .expect(200)); + + it('should return 401 if apikey handler throws exception', async () => + request(app) + .get(`${basePath}/api_key`) + .set('X-API-Key', 'test') + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.equals( + 'custom api key handler failed', + ); + })); + + it('should return 401 if apikey handler returns false', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.ApiKeyAuth = function(req, scopes, schema) { + expect(scopes) + .to.be.an('array') + .with.length(0); + return false; + }; + return request(app) + .get(`${basePath}/api_key`) + .set('X-API-Key', 'test') + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.equals('unauthorized'); + }); + }); + + it('should return 401 if apikey handler returns Promise with false', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.ApiKeyAuth = function(req, scopes, schema) { + expect(scopes) + .to.be.an('array') + .with.length(0); + return Promise.resolve(false); + }; + return request(app) + .get(`${basePath}/api_key`) + .set('X-API-Key', 'test') + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.equals('unauthorized'); + }); + }); + + it('should return 401 if apikey handler returns Promise reject with custom message', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.ApiKeyAuth = (req, scopes, schema) => { + expect(scopes) + .to.be.an('array') + .with.length(0); + return Promise.reject(new Error('rejected promise')); + }; + return request(app) + .get(`${basePath}/api_key`) + .set('X-API-Key', 'test') + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.equals('rejected promise'); + }); + }); + + it('should return 401 if apikey header is missing', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.ApiKeyAuth = (req, scopes, schema) => true; + return request(app) + .get(`${basePath}/api_key`) + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include('X-API-Key'); + }); + }); + + it('should return 200 if apikey header exists and handler returns true', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.ApiKeyAuth = function( + req, + scopes, + schema: OpenAPIV3.ApiKeySecurityScheme, + ) { + expect(schema.type).to.equal('apiKey'); + expect(schema.in).to.equal('header'); + expect(schema.name).to.equal('X-API-Key'); + expect(scopes) + .to.be.an('array') + .with.length(0); + return true; + }; + return request(app) + .get(`${basePath}/api_key`) + .set('X-API-Key', 'test') + .expect(200); + }); + + it('should return 404 if apikey header exists and handler returns true but path doesnt exist', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.ApiKeyAuth = ( + req, + scopes, + schema: OpenAPIV3.ApiKeySecurityScheme, + ) => { + expect(schema.type).to.equal('apiKey'); + expect(schema.in).to.equal('header'); + expect(schema.name).to.equal('X-API-Key'); + expect(scopes) + .to.be.an('array') + .with.length(0); + return true; + }; + return request(app) + .get(`${basePath}/api_key_but_invalid_path`) + .set('X-API-Key', 'test') + .expect(404); + }); + + it('should return 401 if auth header is missing for basic auth', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.BasicAuth = (req, scopes, schema) => true; + return request(app) + .get(`${basePath}/basic`) + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include('Authorization'); + }); + }); + + it('should return 401 if auth header has malformed basic auth', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.BasicAuth = (req, scopes, schema) => true; + return request(app) + .get(`${basePath}/basic`) + .set('Authorization', 'XXXX') + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include( + "Authorization header with scheme 'Basic' required", + ); + }); + }); + + it('should return 401 if auth header is missing for bearer auth', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.BearerAuth = (req, scopes, schema) => true; + return request(app) + .get(`${basePath}/bearer`) + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include('Authorization'); + }); + }); + + it('should return 401 if auth header has malformed bearer auth', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.BearerAuth = (req, scopes, schema) => true; + return request(app) + .get(`${basePath}/bearer`) + .set('Authorization', 'XXXX') + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include( + "Authorization header with scheme 'Bearer' required", + ); + }); + }); + + it('should return 200 if bearer auth succeeds', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.BearerAuth = ( + req, + scopes, + schema: OpenAPIV3.HttpSecurityScheme, + ) => { + expect(schema.type).to.equal('http'); + expect(schema.scheme).to.equal('bearer'); + expect(scopes) + .to.be.an('array') + .with.length(0); + return true; + }; + return request(app) + .get(`${basePath}/bearer`) + .set('Authorization', 'Bearer XXXX') + .expect(200); + }); + + it('should return 200 if oauth2 auth succeeds', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.OAuth2 = function( + req, + scopes, + schema: OpenAPIV3.OAuth2SecurityScheme, + ) { + expect(schema.type).to.equal('oauth2'); + expect(schema).to.have.property('flows'); + expect(scopes) + .to.be.an('array') + .with.length(2); + + return true; + }; + return request(app) + .get(`${basePath}/oauth2`) + .expect(200); + }); + + it('should return 403 if oauth2 handler throws 403', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.OAuth2 = function( + req, + scopes, + schema: OpenAPIV3.OAuth2SecurityScheme, + ) { + expect(schema.type).to.equal('oauth2'); + expect(schema).to.have.property('flows'); + expect(scopes) + .to.be.an('array') + .with.length(2); + + throw { status: 403, message: 'forbidden' }; + }; + return request(app) + .get(`${basePath}/oauth2`) + .expect(403) + .then(r => { + const body = r.body; + expect(r.body.message).to.equal('forbidden'); + }); + }); + + it('should return 200 if openid auth succeeds', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.OpenID = ( + req, + scopes, + schema: OpenAPIV3.OpenIdSecurityScheme, + ) => { + expect(schema.type).to.equal('openIdConnect'); + expect(schema).to.have.property('openIdConnectUrl'); + expect(scopes) + .to.be.an('array') + .with.length(2); + + return true; + }; + return request(app) + .get(`${basePath}/openid`) + .expect(200); + }); + + it('should return 500 if security handlers are defined, but not for all securities', async () => { + const validateSecurity = eovConf.validateSecurity; + delete validateSecurity.handlers.OpenID; + validateSecurity.handlers.Test = ( + req, + scopes, + schema: OpenAPIV3.OpenIdSecurityScheme, + ) => { + expect(schema.type).to.equal('openIdConnect'); + expect(schema).to.have.property('openIdConnectUrl'); + expect(scopes) + .to.be.an('array') + .with.length(2); + + return true; + }; + return request(app) + .get(`${basePath}/openid`) + .expect(500) + .then(r => { + const body = r.body; + const msg = "a security handler for 'OpenID' does not exist"; + expect(body.message).to.equal(msg); + expect(body.errors[0].message).to.equal(msg); + expect(body.errors[0].path).to.equal(`${basePath}/openid`); + }); + }); + + it('should return 500 if scopes are no allowed', async () => + request(app) + .get(`${basePath}/api_key_with_scopes`) + .set('X-Api-Key', 'XXX') + .expect(500) + .then(r => { + const body = r.body; + expect(body.message).to.equal( + "scopes array must be empty for security type 'http'", + ); + })); + + it('should return 200 if api_key or anonymous and no api key is supplied', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.ApiKeyAuth = ((req, scopes, schema) => true); + return request(app) + .get(`${basePath}/api_key_or_anonymous`) + .expect(200); + }); + + it('should return 200 if api_key or anonymous and api key is supplied', async () => { + const validateSecurity = eovConf.validateSecurity; + validateSecurity.handlers.ApiKeyAuth = ((req, scopes, schema) => true); + return request(app) + .get(`${basePath}/api_key_or_anonymous`) + .set('x-api-key', 'XXX') + .expect(200); + }); +});