jackson/npm/test/db/db.test.ts

531 lines
13 KiB
TypeScript

import { DatabaseOption, EncryptionKey, Storable, DatabaseDriver } from '../../src/typings';
import tap from 'tap';
import DB from '../../src/db/db';
import { randomBytes } from 'crypto';
const encryptionKey: EncryptionKey = 'I+mnyTixBoNGu0OtpG0KXJSunoPTiWMb';
const dbObjs: { [key: string]: DatabaseDriver } = {};
const connectionStores: Storable[] = [];
const ttlStores: Storable[] = [];
const ttl = 2;
const record1 = {
id: '1',
name: 'Deepak',
city: 'London',
};
const record2 = {
id: '2',
name: 'Sama',
city: 'London',
};
const record3 = {
id: '3',
name: 'Samuel Jackson',
city: 'Delhi',
};
const records = [record1, record2, record3];
const memDbConfig = <DatabaseOption>{
engine: 'mem',
ttl: 1,
pageLimit: 2,
};
const redisDbConfig = <DatabaseOption>{
engine: 'redis',
url: 'redis://localhost:6379',
pageLimit: 2,
};
const postgresDbConfig = <DatabaseOption>{
engine: 'sql',
url: 'postgresql://postgres:postgres@localhost:5432/postgres',
type: 'postgres',
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
};
const mongoDbConfig = <DatabaseOption>{
engine: 'mongo',
url: 'mongodb://localhost:27017/jackson',
pageLimit: 2,
};
const mysqlDbConfig = <DatabaseOption>{
engine: 'sql',
url: 'mysql://root:mysql@localhost:3307/mysql',
type: 'mysql',
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
};
const planetscaleDbConfig = <DatabaseOption>{
engine: 'planetscale',
url: process.env.PLANETSCALE_URL,
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
// ssl: {
// rejectUnauthorized: true,
// },
};
const mariadbDbConfig = <DatabaseOption>{
engine: 'sql',
url: 'mariadb://root@localhost:3306/mysql',
type: 'mariadb',
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
};
const mssqlDbConfig = <DatabaseOption>{
engine: 'sql',
type: 'mssql',
url: 'sqlserver://localhost:1433;database=master;username=sa;password=123ABabc!',
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
};
const dynamoDbConfig = <DatabaseOption>{
engine: 'dynamodb',
url: process.env.DYNAMODB_URL,
ttl: 1,
cleanupLimit: 10,
pageLimit: 2,
dynamodb: {
region: 'us-east-1',
readCapacityUnits: 5,
writeCapacityUnits: 5,
},
};
const dbs = [
{
...memDbConfig,
},
{
...memDbConfig,
encryptionKey,
},
{
...redisDbConfig,
},
{
...redisDbConfig,
encryptionKey,
},
{
...postgresDbConfig,
},
{
...postgresDbConfig,
encryptionKey,
},
{
...mongoDbConfig,
},
{
...mongoDbConfig,
encryptionKey,
},
{
...mysqlDbConfig,
},
{
...mysqlDbConfig,
encryptionKey,
},
{
...mariadbDbConfig,
},
{
...mariadbDbConfig,
encryptionKey,
},
{
...mssqlDbConfig,
},
{
...mssqlDbConfig,
encryptionKey,
},
];
if (process.env.PLANETSCALE_URL) {
dbs.push(
{
...planetscaleDbConfig,
},
{
...planetscaleDbConfig,
encryptionKey,
}
);
}
if (process.env.DYNAMODB_URL) {
dbs.push(
{
...dynamoDbConfig,
},
{
...dynamoDbConfig,
encryptionKey,
}
);
}
tap.before(async () => {
for (const idx in dbs) {
const opts = dbs[idx];
const db = await DB.new(opts, true);
dbObjs[opts.engine! + (opts.type ? opts.type : '')] = db;
const randomSession = Date.now();
connectionStores.push(db.store('saml:config:' + randomSession + randomBytes(4).toString('hex')));
ttlStores.push(db.store('oauth:session:' + randomSession + randomBytes(4).toString('hex'), ttl));
}
});
tap.teardown(async () => {
process.exit(0);
});
tap.test('dbs', async () => {
for (const idx in connectionStores) {
const connectionStore = connectionStores[idx];
const ttlStore = ttlStores[idx];
const dbEngine = dbs[idx].engine!;
let dbType = dbEngine;
if (dbs[idx].type) {
dbType += ': ' + dbs[idx].type;
}
tap.test('put(): ' + dbType, async () => {
await connectionStore.put(
record1.id,
record1,
{
// secondary index on city
name: 'city',
value: record1.city,
},
{
// secondary index on name
name: 'name',
value: record1.name,
}
);
// wait 100ms to ensure that the record is written with a different timestamp
await new Promise((resolve) => setTimeout(resolve, 100));
await connectionStore.put(
record2.id,
record2,
{
// secondary index on city
name: 'city',
value: record2.city,
},
{
// secondary index on name
name: 'name',
value: record2.name,
}
);
// wait 100ms to ensure that the record is written with a different timestamp
await new Promise((resolve) => setTimeout(resolve, 100));
await connectionStore.put(
record3.id,
record3,
{
// secondary index on city
name: 'city',
value: record3.city,
},
{
// secondary index on name
name: 'name',
value: record3.name,
}
);
});
tap.test('get(): ' + dbType, async (t) => {
const ret1 = await connectionStore.get(record1.id);
const ret2 = await connectionStore.get(record2.id);
t.same(ret1, record1, 'unable to get record1');
t.same(ret2, record2, 'unable to get record2');
});
tap.test('getAll(): ' + dbType, async (t) => {
const testMessage =
dbType === 'dynamodb' // dynamodb doesn't support sort order
? 'should return all the records upto options.pageLimit'
: 'should return all the records upto options.pageLimit in DESC order by creation time';
const wanted = dbType === 'dynamodb' ? records.slice(1, 3) : [...records].reverse().slice(0, 2);
// getAll without pagination params
t.same((await connectionStore.getAll()).data, wanted, `without pagination params ` + testMessage);
// getAll with pagination params
t.same((await connectionStore.getAll(0, 2)).data, wanted, `with pagination params ` + testMessage);
// getAll with pageLimit set to 0
t.same((await connectionStore.getAll(0, 0)).data, wanted, `with pageLimit set to 0 ` + testMessage);
// getAll with pageLimit > options.pageLimit
t.same(
(await connectionStore.getAll(0, 3)).data,
wanted,
`with pageLimit > options.pageLimit ` + testMessage
);
const oneRecordWithPagination = await connectionStore.getAll(0, 1);
t.same(
oneRecordWithPagination.data.length,
1,
"getAll pagination should get only 1 record, order doesn't matter"
);
const secondRecordWithPagination = await connectionStore.getAll(
1,
1,
oneRecordWithPagination.pageToken
);
t.same(
secondRecordWithPagination.data.length,
1,
"getAll pagination should get only 1 record, order doesn't matter"
);
if (!dbEngine.startsWith('dynamodb')) {
const { data: sortedRecordsAsc } = await connectionStore.getAll(0, 2, undefined, 'ASC');
t.match(sortedRecordsAsc, [record1, record2], 'records are sorted in ASC order');
const { data: sortedRecordsDesc } = await connectionStore.getAll(0, 2, undefined, 'DESC');
t.match(sortedRecordsDesc, [record3, record2], 'records are sorted in DESC order');
}
});
tap.test('getByIndex(): ' + dbType, async (t) => {
const ret1 = await connectionStore.getByIndex({
name: 'name',
value: record1.name,
});
const ret2 = await connectionStore.getByIndex({
name: 'city',
value: record1.city,
});
t.same(ret1.data, [record1], 'unable to get index "name"');
t.same(
ret2.data.sort((a, b) => a.id.localeCompare(b.id)),
[record1, record2].sort((a, b) => a.id.localeCompare(b.id)),
'unable to get index "city"'
);
const ret3 = await connectionStore.getByIndex(
{
name: 'city',
value: record1.city,
},
0,
1
);
const numRec = dbEngine === 'dynamodb' ? 2 : 1;
t.same(
ret3.data.length,
numRec,
`getByIndex pagination should get only ${numRec} record, order doesn't matter`
);
const ret4 = await connectionStore.getByIndex(
{
name: 'city',
value: record1.city,
},
1,
1,
ret3.pageToken
);
t.same(
ret4.data.length,
numRec,
`getByIndex pagination should get only ${numRec} record, order doesn't matter`
);
t.same(
ret2.data.sort((a, b) => a.id.localeCompare(b.id)),
dbEngine === 'dynamodb'
? ret3.data.sort((a, b) => a.id.localeCompare(b.id))
: [ret3.data[0], ret4.data[0]].sort((a, b) => a.id.localeCompare(b.id)),
'getByIndex pagination for index "city" failed'
);
// sort order tests
if (!dbEngine.startsWith('dynamodb') && !dbEngine.startsWith('redis')) {
const { data: sortedRecordsAsc } = await connectionStore.getByIndex(
{
name: 'city',
value: record1.city,
},
undefined,
undefined,
undefined,
'ASC'
);
t.match(sortedRecordsAsc, [record1, record2], 'records are sorted in ASC order');
const { data: sortedRecordsDesc } = await connectionStore.getByIndex(
{
name: 'city',
value: record2.city,
},
undefined,
undefined,
undefined,
'DESC'
);
t.match(sortedRecordsDesc, [record2, record1], 'records are sorted in DESC order');
}
});
tap.test('getCount(): ' + dbType, async (t) => {
if (dbEngine !== 'sql' && dbEngine !== 'mongo' && dbEngine !== 'planetscale') {
console.log(`skipping getCount test for ${dbEngine}`);
return;
}
const count = await connectionStore.getCount();
t.equal(count, records.length);
const countByIndex = await connectionStore.getCount({ name: 'name', value: record1.name });
t.equal(countByIndex, 1);
});
tap.test('delete(): ' + dbType, async (t) => {
await connectionStore.delete(record1.id);
const ret0 = await connectionStore.getByIndex({
name: 'city',
value: record1.city,
});
t.same(ret0.data, [record2], 'unable to get index "city" after delete');
await connectionStore.delete(record2.id);
const ret1 = await connectionStore.get(record1.id);
const ret2 = await connectionStore.get(record2.id);
const ret3 = await connectionStore.getByIndex({
name: 'name',
value: record1.name,
});
const ret4 = await connectionStore.getByIndex({
name: 'city',
value: record1.city,
});
t.same(ret1, null, 'delete for record1 failed');
t.same(ret2, null, 'delete for record2 failed');
t.same(ret3.data, [], 'delete for record1 failed');
t.same(ret4.data, [], 'delete for record2 failed');
});
tap.test('ttl put(): ' + dbType, async () => {
await ttlStore.put(record1.id, record1);
await ttlStore.put(record2.id, record2);
});
tap.test('ttl get(): ' + dbType, async (t) => {
const ret1 = await ttlStore.get(record1.id);
const ret2 = await ttlStore.get(record2.id);
t.same(ret1, record1, 'unable to get record1');
t.same(ret2, record2, 'unable to get record2');
});
tap.test('ttl expiry: ' + dbType, async (t) => {
// mongo runs ttl task every 60 seconds
if (dbEngine.startsWith('mongo')) {
return;
}
await new Promise((resolve) => setTimeout(resolve, (2 * ttl + 0.5) * 1000));
const ret1 = await ttlStore.get(record1.id);
const ret2 = await ttlStore.get(record2.id);
t.same(ret1, null, 'ttl for record1 failed');
t.same(ret2, null, 'ttl for record2 failed');
});
tap.test('deleteMany(): ' + dbType, async (t) => {
await connectionStore.put(
record1.id,
record1,
{
name: 'city',
value: record1.city,
},
{
name: 'name',
value: record1.name,
}
);
await connectionStore.put(
record2.id,
record2,
{
name: 'city',
value: record2.city,
},
{
name: 'name',
value: record2.name,
}
);
await connectionStore.deleteMany([record1.id, record2.id]);
const ret1 = await connectionStore.get(record1.id);
const ret2 = await connectionStore.get(record2.id);
t.same(ret1, null);
t.same(ret2, null);
const ret3 = await connectionStore.getByIndex({
name: 'name',
value: record1.name,
});
const ret4 = await connectionStore.getByIndex({
name: 'city',
value: record1.city,
});
t.same(ret3.data, []);
t.same(ret4.data, []);
});
}
tap.test('close():', async () => {
for (const [, value] of Object.entries(dbObjs)) {
await value.close();
}
});
});