ravel
Version:
Ravel Rapid Application Development Framework
326 lines (298 loc) • 11.6 kB
JavaScript
describe('db/database', () => {
let Ravel, DatabaseProvider, ravelApp, Resource, Module, inject, mysqlProvider, postgresProvider, mysqlConnection, postgresConnection;
beforeEach(() => {
Ravel = require('../../lib/ravel');
ravelApp = new Ravel();
// choose random port to support jest parallelization
ravelApp.set('port', Math.floor(Math.random() * 10000) + 10000);
ravelApp.set('keygrip keys', ['abc']);
ravelApp.set('log level', ravelApp.$log.NONE);
DatabaseProvider = Ravel.DatabaseProvider;
Resource = Ravel.Resource;
Module = Ravel.Module;
inject = Ravel.inject;
// mock a couple of providers for us to use
mysqlProvider = ravelApp.registerProvider(DatabaseProvider, 'mysql');
postgresProvider = ravelApp.registerProvider(DatabaseProvider, 'postgres');
// mock stuff
mysqlConnection = Object.create(null);
postgresConnection = Object.create(null);
mysqlProvider.getTransactionConnection = jest.fn(function () {
return Promise.resolve(mysqlConnection);
});
postgresProvider.getTransactionConnection = jest.fn(function () {
return Promise.resolve(postgresConnection);
});
mysqlProvider.exitTransaction = jest.fn(function () {
return Promise.resolve();
});
postgresProvider.exitTransaction = jest.fn(function () {
return Promise.resolve();
});
});
describe('#middleware()', () => {
it('should populate context.transaction with an empty dictionary of database connection objects when no database providers are registered.', async () => {
class R {
getAll (ctx) {
expect(ctx.transaction).toEqual({});
}
}
ravelApp.load(R);
await ravelApp.init();
await request(ravelApp.callback).get('/');
});
it('should populate req.transaction with a dictionary of open database connections when providers are registered', async () => {
class R {
.transaction
getAll (ctx) {
expect(mysqlProvider.getTransactionConnection).toHaveBeenCalledTimes(1);
expect(postgresProvider.getTransactionConnection).toHaveBeenCalledTimes(1);
expect(ctx).to.have.a.property('transaction').toEqual({
mysql: mysqlConnection,
postgres: postgresConnection
});
}
}
ravelApp.load(R);
await ravelApp.init();
await request(ravelApp.callback).get('/');
await ravelApp.listen();
await ravelApp.close();
});
it('should populate req.transaction with a dictionary of only the correct open database connections when providers are requested by name', async () => {
.transaction('postgres')
class R {
getAll (ctx) {
expect(mysqlProvider.getTransactionConnection).not.toHaveBeenCalled;
expect(postgresProvider.getTransactionConnection).toHaveBeenCalled;
expect(ctx.transaction).toEqual({
postgres: postgresConnection
});
}
}
ravelApp.load(R);
await ravelApp.init();
await request(ravelApp.callback).get('/');
});
it('should throw an Error if any of the registered database providers fails to provide a connection', async () => {
postgresProvider.getTransactionConnection = jest.fn(function () {
return Promise.reject(new Error());
});
class R {
.transaction('postgres')
getAll (ctx) { }
}
ravelApp.load(R);
await ravelApp.init();
const res = await request(ravelApp.callback).get('/');
expect(res.status).toBe(500);
});
it('should end all open transactions (close/commit connections) when the middleware chain wraps up', async () => {
class R {
.transaction
getAll (ctx) { }
}
ravelApp.load(R);
await ravelApp.init();
await request(ravelApp.callback).get('/');
expect(mysqlProvider.exitTransaction).toHaveBeenCalledWith(mysqlConnection, true);
expect(postgresProvider.exitTransaction).toHaveBeenCalledWith(postgresConnection, true);
});
it('should end all open transactions (close/rollback connections) when an exception is thrown in the middleware chain', async () => {
class R {
.transaction
getAll (ctx) {
throw new ravelApp.$err.NotFound();
}
}
ravelApp.load(R);
await ravelApp.init();
const res = await request(ravelApp.callback).get('/');
expect(mysqlProvider.exitTransaction).toHaveBeenCalledWith(mysqlConnection, false);
expect(postgresProvider.exitTransaction).toHaveBeenCalledWith(postgresConnection, false);
expect(res.status).toBe(404);
});
it('should respond with HTTP 500 INTERNAL SERVER ERROR when any open transactions fail to close/commit when res.end() is called', async () => {
mysqlProvider.exitTransaction = jest.fn(function () {
return Promise.reject(new Error());
});
class R {
.transaction
getAll (ctx) { }
}
ravelApp.load(R);
await ravelApp.init();
const res = await request(ravelApp.callback).get('/');
expect(mysqlProvider.exitTransaction).toHaveBeenCalledWith(mysqlConnection, false);
expect(postgresProvider.exitTransaction).toHaveBeenCalledWith(postgresConnection, false);
expect(res.status).toBe(500);
});
});
describe('#scoped()', () => {
it('should populate scoped context with a dictionary of open database connections when providers are registered', async () => {
class M {
constructor ($db) {
this.$db = $db;
}
method () {
return this.$db.scoped(async function (ctx) {
expect(mysqlProvider.getTransactionConnection).toHaveBeenCalled;
expect(postgresProvider.getTransactionConnection).toHaveBeenCalled;
expect(ctx.transaction).toEqual({
mysql: mysqlConnection,
postgres: postgresConnection
});
});
}
}
ravelApp.load(M);
await ravelApp.init();
return ravelApp.module('test').method();
});
it('should populate scoped context with a dictionary of the correct open database connections when providers are requested by name', async () => {
class M {
constructor ($db) {
this.$db = $db;
}
method () {
return this.$db.scoped('postgres', async function (ctx) {
expect(mysqlProvider.getTransactionConnection).not.toHaveBeenCalled;
expect(postgresProvider.getTransactionConnection).toHaveBeenCalled;
expect(ctx.transaction).toEqual({
postgres: postgresConnection
});
});
}
}
ravelApp.load(M);
await ravelApp.init();
return ravelApp.module('test').method();
});
it('should throw an exception if any of the registered database providers fails to provide a connection', async () => {
mysqlProvider.getTransactionConnection = jest.fn(function () {
return Promise.reject(new Error());
});
class M {
constructor ($db) {
this.$db = $db;
}
method () {
return this.$db.scoped('mysql', async function (ctx) {
expect(mysqlProvider.getTransactionConnection).toHaveBeenCalled;
expect(postgresProvider.getTransactionConnection).not.toHaveBeenCalled;
expect(ctx.transaction).toEqual({
postgres: postgresConnection
});
});
}
}
ravelApp.load(M);
await ravelApp.init();
await expect(ravelApp.module('test').method()).rejects.toThrow(Error);
});
it('should end all open transactions (close/commit connections) when inGen is finished executing', async () => {
class M {
constructor ($db) {
this.$db = $db;
}
method () {
return this.$db.scoped(async function (ctx) {
expect(mysqlProvider.getTransactionConnection).toHaveBeenCalled;
expect(postgresProvider.getTransactionConnection).toHaveBeenCalled;
expect(ctx.transaction).toEqual({
mysql: mysqlConnection,
postgres: postgresConnection
});
});
}
}
ravelApp.load(M);
await ravelApp.init();
await ravelApp.module('test').method();
expect(mysqlProvider.exitTransaction).toHaveBeenCalledWith(mysqlConnection, true);
expect(postgresProvider.exitTransaction).toHaveBeenCalledWith(postgresConnection, true);
});
it('should end all open transactions (close/rollback connections) when inGen throws an exception', async () => {
class M {
constructor ($db) {
this.$db = $db;
}
method () {
return this.$db.scoped(async function (ctx) {
expect(mysqlProvider.getTransactionConnection).toHaveBeenCalled;
expect(postgresProvider.getTransactionConnection).toHaveBeenCalled;
expect(ctx.transaction).toEqual({
mysql: mysqlConnection,
postgres: postgresConnection
});
throw new Error();
});
}
}
ravelApp.load(M);
await ravelApp.init();
try {
await ravelApp.module('test').method();
} catch (err) {
// do nothing
}
expect(mysqlProvider.exitTransaction).toHaveBeenCalledWith(mysqlConnection, false);
expect(postgresProvider.exitTransaction).toHaveBeenCalledWith(postgresConnection, false);
});
it('should reject its returned promise when any connection fails to exit cleanly', async () => {
postgresProvider.exitTransaction = jest.fn(function () {
return Promise.reject(new Error()); // eslint-disable-line prefer-promise-reject-errors
});
class M {
constructor ($db) {
this.$db = $db;
}
method () {
return this.$db.scoped(async function (ctx) {});
}
}
ravelApp.load(M);
await ravelApp.init();
await expect(ravelApp.module('test').method()).rejects.toThrow(Error);
// wait a bit to check closers, since the method will reject the
// second a connection-closer fails, but will still run the others afterwards.
await new Promise((resolve) => setTimeout(resolve, 100));
expect(mysqlProvider.exitTransaction).toHaveBeenCalledWith(mysqlConnection, false);
expect(postgresProvider.exitTransaction).toHaveBeenCalledWith(postgresConnection, false);
});
it('should populate req.transaction with an empty dictionary if no providers are registered', async () => {
ravelApp = new Ravel();
ravelApp.set('keygrip keys', ['abc']);
ravelApp.set('log level', ravelApp.$log.NONE);
.transaction('postgres')
class R {
getAll (ctx) {
expect(ctx.transaction).toEqual({});
}
}
ravelApp.load(R);
await ravelApp.init();
await request(ravelApp.callback).get('/');
});
});
});