ravel
Version:
Ravel Rapid Application Development Framework
577 lines (498 loc) • 20.1 kB
JavaScript
describe('Ravel', () => {
let Ravel, app;
beforeEach(() => {
jest.resetModules();
Ravel = require('../../lib/ravel');
app = new Ravel();
app.set('keygrip keys', ['abc']);
app.set('log level', app.$log.NONE);
});
describe('#routes', () => {
it('should throw an exception if called before app.init()', () => {
@Ravel.Routes('/')
class Test {}
app.load(Test);
expect(() => app.routes('/')).toThrow(app.$err.General);
});
});
// Testing how Ravel loads routes
describe('#load', () => {
describe('@Routes', () => {
it('should register Routes modules for instantiation and initialization in Ravel.init', async () => {
const spy = jest.fn();
@Ravel.Routes('/')
class Test {
method () {
spy();
}
}
app.load(Test);
await app.init();
expect(app.routes('/')).toBeDefined();
app.routes('/').method();
expect(spy).toHaveBeenCalled();
});
it('should throw a Ravel.$err.IllegalValue error when clients attempt to register a routes module without a basePath', async () => {
expect(() => {
@Ravel.Routes
class Test {}
app.load(Test);
}).toThrowError(app.$err.IllegalValue);
});
it('should throw a Ravel.$err.DuplicateEntry error when clients attempt to register multiple routes modules with the same basePath', () => {
@Ravel.Routes('/')
class Test {}
@Ravel.Routes('/')
class Test2 {}
expect(() => app.load(Test, Test2)).toThrowError(app.$err.DuplicateEntry);
});
it('should throw a Ravel.$err.IllegalValue error when clients attempt to register a routes module without appropriate decoration', async () => {
class Test {}
expect(() => app.load(Test)).toThrowError(app.$err.IllegalValue);
});
it('should load and instantiate routes modules, performing dependency injection of core services', async () => {
@Ravel.Routes('/')
@Ravel.inject('$app', '$err', '$log', '$kvstore', '$params', '$db')
class Test {
constructor ($app, $err, $log, $kvstore, $params, $db) {
this.$app = $app;
this.$err = $err;
this.$log = $log;
this.$kvstore = $kvstore;
this.$params = $params;
this.$db = $db;
}
}
app.load(Test);
await app.init();
const instance = app.routes('/');
expect(instance).toBeDefined();
expect(instance.$app).toEqual(app);
expect(instance.$err).toEqual(app.$err);
expect(instance.$log).toBeDefined();
expect(instance.$log).toHaveProperty('trace');
expect(instance.$log).toHaveProperty('verbose');
expect(instance.$log).toHaveProperty('debug');
expect(instance.$log).toHaveProperty('info');
expect(instance.$log).toHaveProperty('warn');
expect(instance.$log).toHaveProperty('error');
expect(instance.$log).toHaveProperty('critical');
expect(instance.$kvstore).toEqual(app.$kvstore);
expect(instance.$params).toBeDefined();
expect(instance.$params).toHaveProperty('get');
expect(instance.$db).toHaveProperty('scoped');
});
it('should facilitate the creation of GET routes via @mapping', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.status = 200;
ctx.body = { id: 3 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).get('/api/test');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 3 });
});
it('should facilitate the creation of GET routes via @mapping with different status codes', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.status = 201;
ctx.body = { id: 3 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).get('/api/test');
expect(response.statusCode).toBe(201);
expect(response.body).toEqual({ id: 3 });
});
it('should facilitate the creation of HEAD routes via @mapping', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.HEAD, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.status = 200;
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).head('/api/test');
expect(response.statusCode).toBe(200);
});
it('should facilitate the creation of POST routes via @mapping', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.POST, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.body = { id: 1 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).post('/api/test');
expect(response.statusCode).toBe(201);
expect(response.body).toEqual({ id: 1 });
});
it('should facilitate the creation of PUT routes via @mapping', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.PUT, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.body = { id: 1 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).put('/api/test');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 1 });
});
it('should facilitate the creation of PATCH routes via @mapping', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.PATCH, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.body = { id: 1 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).patch('/api/test');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 1 });
});
it('should facilitate the creation of DELETE routes via @mapping', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.DELETE, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.body = { id: 1 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).delete('/api/test');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 1 });
});
it('should facilitate the creation of catch-all routes via @mapping', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test', { catchAll: true })
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.body = { id: 1 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).get('/api/test/something/else');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 1 });
});
it('should deliver query parameters to ctx.query', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.body = ctx.query;
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).get('/api/test?param1=foo¶m2=bar');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ param1: 'foo', param2: 'bar' });
});
it('should prevent catch-all routes from eating overlapping non-catch-all ones', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test', { catchAll: true })
@Ravel.Routes.before('middleware1', 'middleware2')
async pathHandler (ctx) {
ctx.body = { id: 1 };
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test/something')
@Ravel.Routes.before('middleware1', 'middleware2')
async anotherPathHandler (ctx) {
ctx.body = { id: 2 };
}
}
app.load(Test);
await app.init();
let response = await request(app.callback).get('/api/test/something/else');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 1 });
response = await request(app.callback).get('/api/test/something');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 2 });
response = await request(app.callback).get('/api/test');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 1 });
});
it('should support the use of @before at the method and class levels', async () => {
const middleware1 = async function (ctx, next) { ctx.body = { id: ctx.params.id }; await next(); };
const middleware2 = async function (ctx, next) { ctx.body.name = 'sean'; await next(); };
@Ravel.Routes('/api')
@Ravel.Routes.before('middleware1')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test/:id')
@Ravel.Routes.before('middleware2')
async pathHandler (ctx) {
ctx.status = 200;
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).get('/api/test/3');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: '3', name: 'sean' });
});
it('should support the use of @mapping without @before', async () => {
@Ravel.Routes('/api')
class Test {
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
async pathHandler (ctx) {
ctx.status = 200;
ctx.body = {};
}
@Ravel.Routes.before('middleware2') // this should just be ignored, since @mapping isn't present
async ignoredHandler (ctx) {
ctx.status = 200;
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).get('/api/test');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({});
});
it('should support the use of @mapping at the class level as well, to denote unsupported routes', async () => {
@Ravel.Routes('/api')
@Ravel.Routes.mapping(Ravel.Routes.GET, '/path') // should respond with NOT_IMPLEMENTED
@Ravel.Routes.mapping(Ravel.Routes.POST, '/another', { status: 404 }) // should respond with 404
class Test {
}
app.load(Test);
await app.init();
let response = await request(app.callback).get('/api/path');
expect(response.statusCode).toBe(501);
response = await request(app.callback).post('/api/another');
expect(response.statusCode).toBe(404);
});
});
it('should support non-async handlers as well', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
pathHandler (ctx) {
ctx.status = 200;
ctx.body = { id: 3 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).get('/api/test');
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({ id: 3 });
});
it('should respond with 404 NOT FOUND if the specified route does not exist', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
pathHandler (ctx) {
ctx.status = 200;
ctx.body = { id: 3 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).get('/api/foobar');
expect(response.statusCode).toBe(404);
});
it('should respond with 405 METHOD NOT ALLOWED if the given verb is not supported by Ravel', async () => {
const middleware1 = async function (ctx, next) { await next(); };
const middleware2 = async function (ctx, next) { await next(); };
@Ravel.Routes('/api')
class Test {
constructor () {
this.middleware1 = middleware1;
this.middleware2 = middleware2;
}
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
@Ravel.Routes.before('middleware1', 'middleware2')
pathHandler (ctx) {
ctx.status = 200;
ctx.body = { id: 3 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).head('/api/test');
expect(response.statusCode).toBe(405);
});
it('should throw a Ravel.$err.IllegalValueError error when clients attempt to use a middleware factory with fewer than the required number of arguments', async () => {
@Ravel.Module('/')
class TestModule {
@Ravel.Module.middleware('middleware1', { factory: true })
middlewareFactory (one, two) {
return async function () {};
}
}
@Ravel.Routes('/')
class TestRoutes {
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
@Ravel.Routes.before('middleware1', 'arg1')
pathHandler (ctx) {
ctx.status = 200;
}
}
app.load(TestModule, TestRoutes);
await expect(app.init()).rejects.toThrow(app.$err.IllegalValueError);
});
it('should throw a Ravel.$err.IllegalValueError error when clients attempt to apply a status to method-level middleware', async () => {
expect(() => {
@Ravel.Routes('/')
class TestRoutes {
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test', { status: 500 })
pathHandler (ctx) {
ctx.status = 200;
}
}
app.load(TestRoutes);
}).toThrow(app.$err.IllegalValueError);
});
it('should throw a Ravel.$err.IllegalValueError error when clients attempt to apply catchAll to class-level middleware', async () => {
expect(() => {
@Ravel.Routes('/')
@Ravel.Routes.mapping(Ravel.Routes.GET, '/foobar', { catchAll: true })
class TestRoutes {
@Ravel.Routes.mapping(Ravel.Routes.GET, '/test')
pathHandler (ctx) {
ctx.status = 200;
}
}
app.load(TestRoutes);
}).toThrow(app.$err.IllegalValueError);
});
it('should throw Ravel.$err.General if ctx.response.body is used in a HEAD route', async () => {
@Ravel.Routes('/api')
class Test {
@Ravel.Routes.mapping(Ravel.Routes.HEAD, '/test')
async pathHandler (ctx) {
ctx.status = 200;
ctx.body = { id: 3 };
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).head('/api/test');
expect(response.statusCode).toBe(500);
});
it('Non-Ravel errors in a HEAD route should produce an HTTP 500', async () => {
@Ravel.Routes('/api')
class Test {
@Ravel.Routes.mapping(Ravel.Routes.HEAD, '/test')
async pathHandler (ctx) {
throw new Error('something went wrong!');
}
}
app.load(Test);
await app.init();
const response = await request(app.callback).head('/api/test');
expect(response.statusCode).toBe(500);
});
});
});